Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
665b591
Pilot: drop in TypeSpec-emitter-generated artifacts for an end-to-end…
Oct 5, 2026
1680935
Add a real hand-written server layer driven by the generated artifacts
Oct 5, 2026
70d046e
Move pilot artifacts under src/ so the Docker build picks them up
Oct 5, 2026
08ca826
pilot: validate against real Storage Queue spec (17 ops), rewire disp…
Oct 5, 2026
910f9fa
pilot: drive real Queue handlers from generated dispatch metadata; pi…
Oct 5, 2026
d4d41f4
pilot: finish storage-queue-real -> storage-queue rename in docs/tests
Oct 5, 2026
f2a735b
Patch real dispatch.middleware.ts in place instead of a parallel pilo…
Oct 5, 2026
748ec17
Use makeOptional overlay in Queue TypeSpec pilot
Oct 6, 2026
c3c3925
Use generated Queue dispatch metadata
Oct 6, 2026
31a8ee5
Move Queue dispatch bridge into Azurite
Oct 6, 2026
1557fdf
Use clientName for Queue operation metadata
Oct 6, 2026
5066e8a
Use TypeSpec Queue serialization specs
Oct 6, 2026
0dc47ed
Use generated Queue serialization helpers
Oct 6, 2026
6a2cf00
Iterate generated Queue operations in dispatcher
Oct 6, 2026
b8a75ef
Regenerate Queue serialization as direct functions
Oct 6, 2026
b43ca11
Regenerate Queue models without anonymous clutter
Oct 6, 2026
959ac1e
Regenerate Queue serialization with shared helpers
Oct 6, 2026
8ce82e1
Use generated Queue XML serialization
Oct 6, 2026
8c30b68
Prove generated Queue HTTP flow end to end
Oct 7, 2026
6058814
Preserve Queue access policy request bodies
Oct 7, 2026
90770e3
Pin superseding Queue emitter provenance
Oct 7, 2026
230f415
Extract the TypeSpec protocol runtime
Oct 7, 2026
8f096be
Consolidate generated Queue metadata
Oct 7, 2026
0060311
Update Queue pilot to final emitter metadata
Oct 7, 2026
22b9cc6
Reject empty Queue numeric parameters
Oct 7, 2026
dbffb56
Document Queue TypeSpec adapters
Oct 7, 2026
8314a22
Preserve Queue numeric constraints
Oct 7, 2026
d92d8dd
Regenerate Queue pilot after emitter review
Oct 8, 2026
7c309ac
Update final Azurite emitter provenance
Oct 8, 2026
16718fe
Pin final Azurite emitter provenance
Oct 8, 2026
e87a0b5
Clarify Queue pilot migration seams
Oct 8, 2026
1db482a
Revert committed Queue reviewer cues
Oct 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 123 additions & 2 deletions src/queue/generated/middleware/HandlerMiddlewareFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,121 @@ import getHandlerByOperation from "../handlers/handlerMappers";
import IHandlers from "../handlers/IHandlers";
import { NextFunction } from "../MiddlewareFactory";
import ILogger from "../utils/ILogger";
import {
operations,
type OperationMetadata
} from "../../typespecPilot/generated/metadata";

function getGeneratedOperation(operation: Operation): OperationMetadata {
// Keep the existing Operation enum as the auth/handler key while TypeSpec owns the wire metadata.
const operationName = Operation[operation];
const metadata = operations.find(
(candidate) => candidate.name === operationName
);
if (metadata === undefined) {
throw new TypeError(
`Generated TypeSpec metadata does not include operation ${operationName}`
);
}
return metadata;
}

function getLegacyParameterName(wireName: string, name: string): string {
if (wireName.toLowerCase() === "x-ms-client-request-id") {
// Existing handlers call this requestId; see src/queue/generated/handlers/IQueueHandler.ts.
return "requestId";
}
if (wireName.toLowerCase() === "visibilitytimeout") {
// Preserve the casing used by the existing message handler option bags.
return "visibilitytimeout";
}
return name;
}

function adaptGeneratedParameters(
metadata: OperationMetadata,
handlerArguments: readonly string[],
parameters: Record<string, any>
): Record<string, any> {
const adapted: Record<string, any> = {};
const options: Record<string, any> = {};

for (const parameter of metadata.parameters) {
const value = parameters[parameter.name];
const legacyName = getLegacyParameterName(
parameter.wireName,
parameter.name
);
if (parameter.required) {
// handlerMappers still pass required values positionally and optional ones in options.
adapted[legacyName] = value;
} else if (value !== undefined) {
options[legacyName] = value;
}
}

if (handlerArguments.includes("options")) {
adapted.options = options;
}
if (metadata.hasRequestBody) {
// Keep body separate for IMessagesHandler.enqueue(body, options, context).
const bodyArgument = handlerArguments.find(
(argument) => argument !== "options" && adapted[argument] === undefined
);
if (bodyArgument !== undefined) {
adapted[bodyArgument] = parameters.body;
} else if (metadata.name === "Queue_SetAccessPolicy") {
// IQueueHandler.setAccessPolicy still expects the XML list as options.queueAcl.
options.queueAcl = parameters.body.items;
}
}

return adapted;
}

function adaptLegacyResponse(
metadata: OperationMetadata,
response: Record<string, any>
): Record<string, any> {
const responseMetadata =
metadata.responses.find(
(candidate) => candidate.statusCode === response.statusCode
) ??
metadata.responses.find((candidate) => candidate.statusCode === "*");
if (responseMetadata === undefined) {
return response;
}

const headerNames = new Set(
responseMetadata.headers.map((header) => header.name)
);
const headers: Record<string, any> = {};
for (const headerName of headerNames) {
if (response[headerName] !== undefined) {
headers[headerName] = response[headerName];
}
}

const adapted: Record<string, any> = {
statusCode: response.statusCode,
headers
};
// Legacy handlers return flat header/body fields; generated serialization expects both nested.
if (responseMetadata.body !== undefined) {
if (Array.isArray(response)) {
adapted.body = { items: response };
} else {
const body: Record<string, any> = {};
for (const [name, value] of Object.entries(response)) {
if (name !== "statusCode" && !headerNames.has(name)) {
body[name] = value;
}
}
adapted.body = body;
}
}
return adapted;
}

/**
* Auto generated. HandlerMiddlewareFactory will accept handlers and create handler middleware.
Expand Down Expand Up @@ -69,10 +184,16 @@ export default class HandlerMiddlewareFactory {

// We assume handlerPath always exists for every generated operation in generated code
const handlerPath = getHandlerByOperation(context.operation)!;
const metadata = getGeneratedOperation(context.operation);
const handlerParameters = adaptGeneratedParameters(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a temporary migration seam for production QueueServer: it reshapes the new flat generated parameters into the split positional/options contract used by the existing handlerMappers and handlers.

metadata,
handlerPath.arguments,
context.handlerParameters!
);

const args = [];
for (const arg of handlerPath.arguments) {
args.push(context.handlerParameters![arg]);
args.push(handlerParameters[arg]);
}
args.push(context);

Expand All @@ -81,7 +202,7 @@ export default class HandlerMiddlewareFactory {
handlerMethod
.apply(handler, args as any)
.then((response: any) => {
context.handlerResponses = response;
context.handlerResponses = adaptLegacyResponse(metadata, response);
})
.then(next)
.catch(next);
Expand Down
54 changes: 44 additions & 10 deletions src/queue/generated/middleware/deserializer.middleware.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import Operation from "../artifacts/operation";
import Specifications from "../artifacts/specifications";
import AutoRestSpecifications from "../artifacts/specifications";
import Context from "../Context";
import DeserializationError from "../errors/DeserializationError";
import OperationMismatchError from "../errors/OperationMismatchError";
import IRequest from "../IRequest";
import { NextFunction } from "../MiddlewareFactory";
import ILogger from "../utils/ILogger";
import { deserialize } from "../utils/serializer";
import { deserializeRequest } from "../../typespecPilot/generated/serialization";
import { NumericConstraintError } from "../../typespecPilot/runtime/serializationRuntime";
import StorageErrorFactory from "../../errors/StorageErrorFactory";

/**
* Deserializer Middleware. Deserialize incoming HTTP request into models.
Expand All @@ -29,7 +32,8 @@ export default function deserializerMiddleware(
context.contextID
);

if (context.operation === undefined) {
const operation = context.operation;
if (operation === undefined) {
const handlerError = new OperationMismatchError();
logger.error(
`DeserializerMiddleware: ${handlerError.message}`,
Expand All @@ -38,20 +42,50 @@ export default function deserializerMiddleware(
return next(handlerError);
}

if (Specifications[context.operation] === undefined) {
logger.warn(
`DeserializerMiddleware: Cannot find deserializer for operation ${
Operation[context.operation]
}`
);
}
const operationName = Operation[operation];

deserialize(context, req, Specifications[context.operation], logger)
deserializeRequest(operationName, req, context)
.then(parameters => {
if (parameters !== undefined) {
return parameters;
}

// Fall back for legacy-only operations until the TypeSpec service describes every route.
const specification = AutoRestSpecifications[operation];
if (specification === undefined) {
logger.warn(
`DeserializerMiddleware: Cannot find deserializer for operation ${operationName}`
);
}
return deserialize(context, req, specification, logger);
})
.then(parameters => {
context.handlerParameters = parameters;
})
.then(next)
.catch(err => {
if (err instanceof NumericConstraintError && err.location === "query") {
// Keep the error shape returned by Queue handlers for out-of-range query values.
const minimum =
err.constraints.min ?? err.constraints.minExclusive;
const maximum =
err.constraints.max ?? err.constraints.maxExclusive;
return next(
StorageErrorFactory.getOutOfRangeQueryParameterValue(
context.contextID,
{
QueryParameterName: err.wireName,
QueryParameterValue: `${err.value}`,
...(minimum === undefined
? {}
: { MinimumAllowed: `${minimum}` }),
...(maximum === undefined
? {}
: { MaximumAllowed: `${maximum}` })
}
)
);
}
const deserializationError = new DeserializationError(err.message);
deserializationError.stack = err.stack;
next(deserializationError);
Expand Down
Loading
Loading