@luvio/generator-ts
Advanced tools
Comparing version 5.11.0 to 5.12.0
@@ -218,2 +218,7 @@ /** | ||
} | ||
// default importStringifier implementation | ||
function defaultImportStringifier(ir) { | ||
const source = 'filename' in ir ? `file:${ir.filename}` : `module:${ir.module}`; | ||
return `<<${source}:${ir.exportedSymbol}:${ir.isType})>>`; | ||
} | ||
/** | ||
@@ -255,4 +260,4 @@ * A Code instance holds a collection of TypeScript code. | ||
* | ||
* @param newChunks | ||
* @returns | ||
* @param newChunks code to be appended | ||
* @returns this | ||
*/ | ||
@@ -287,22 +292,19 @@ push(...newChunks) { | ||
equals(comparison) { | ||
return (this.chunks.length === comparison.chunks.length && | ||
this.chunks.every((chunkA, i) => { | ||
const chunkB = comparison.chunks[i]; | ||
if (isImportableReference(chunkA)) { | ||
if (isImportableReference(chunkB)) { | ||
// replace with generic deep equality function if ImportableReference grows or changes | ||
return (chunkA.exportedSymbol === chunkB.exportedSymbol && | ||
(('filename' in chunkA && | ||
'filename' in chunkB && | ||
chunkA.filename === chunkB.filename) || | ||
('module' in chunkA && | ||
'module' in chunkB && | ||
chunkA.module === chunkB.module)) && | ||
chunkA.isType === chunkB.isType); | ||
} | ||
return false; | ||
} | ||
return chunkA === chunkB; | ||
})); | ||
return this.toString() === comparison.toString(); | ||
} | ||
/** | ||
* Returns the Code as a single string. | ||
* | ||
* @param options | ||
* importStringifier - function to convert ImportableReferences to strings. | ||
* Defaults to an internal function whose only guarantee is that equivalent | ||
* ImportableReferencesit will result in equivalent strings. | ||
* @returns string representation of the code | ||
*/ | ||
toString(options = {}) { | ||
const irToString = options.importStringifier || defaultImportStringifier; | ||
return this.chunks | ||
.map((chunk) => (isImportableReference(chunk) ? irToString(chunk) : chunk)) | ||
.join(''); | ||
} | ||
} | ||
@@ -451,8 +453,19 @@ /** | ||
*/ | ||
class File extends Code { | ||
class File { | ||
constructor(filename) { | ||
super(); | ||
this.filename = filename; | ||
this.code = new Code(); | ||
} | ||
/** | ||
* Appends new chunks of code to this list. Parameters are handled as in | ||
* Code.push(). | ||
* | ||
* @param newChunks code to be appended | ||
* @returns this | ||
*/ | ||
push(...newChunks) { | ||
this.code.push(...newChunks); | ||
return this; | ||
} | ||
/** | ||
* Returns the code for this File, with ImportableReferences resolved. | ||
@@ -466,6 +479,6 @@ * | ||
const imports = new Imports(this.filename); | ||
const resolvedString = this.chunks | ||
.map((chunk) => (isImportableReference(chunk) ? imports.localSymbolFor(chunk) : chunk)) | ||
.join(''); | ||
const code = imports.toString() + resolvedString; | ||
const executableCode = this.code.toString({ | ||
importStringifier: (ir) => imports.localSymbolFor(ir), | ||
}); | ||
const code = imports.toString() + executableCode; | ||
return options.prettified ? prettier.format(code, { parser: 'typescript' }) : code; | ||
@@ -494,171 +507,79 @@ } | ||
function jsonSchemaFor(type, additionalProperties = false) { | ||
/** | ||
* Returns the JSON schema for a specified type. | ||
* | ||
* @param type the type | ||
* @returns JSON schema for type | ||
*/ | ||
function jsonSchemaFor(type) { | ||
if (type.type === 'any') { | ||
return code `true`; | ||
} | ||
let jsonSchemaBuilder; | ||
if (type.inherits.length > 0) { | ||
jsonSchemaBuilder = new AllOfJsonSchemaBuilder(type); | ||
// arbitrary type with inheritance => allOf(the inherited types + the type w/o inheritance) | ||
else if (type.inherits.length > 0) { | ||
return code `{allOf:[${schemasFor(...type.inherits, Object.assign({}, type, { inherits: [] }))}]}`; | ||
} | ||
else if (type.type === 'union') { | ||
jsonSchemaBuilder = new OneOfJsonSchemaBuilder(type, additionalProperties); | ||
return code `{anyOf:[${schemasFor(...type.anyOf)}]}`; | ||
} | ||
else if (type.type === 'object') { | ||
// discriminated object => anyOf the discriminated types | ||
if (type.discriminator !== undefined) { | ||
jsonSchemaBuilder = new DiscriminatedObjectJsonSchemaBuilder(type, additionalProperties); | ||
return code `{anyOf:[${schemasFor(...type.discriminatorValueMapping.map((val) => val.type))}]}`; | ||
} | ||
else { | ||
jsonSchemaBuilder = new ObjectJsonSchemaBuilder(type, additionalProperties); | ||
} | ||
return buildObjectSchema(type); | ||
} | ||
else if (type.type === 'array') { | ||
jsonSchemaBuilder = new ArrayJsonSchemaBuilder(type, additionalProperties); | ||
return code `{type:'array',items:${jsonSchemaFor(type.items)}}`; | ||
} | ||
else if (['string', 'date', 'time', 'datetime', 'datetime-only'].some((t) => t === type.type)) { | ||
jsonSchemaBuilder = new StringJsonSchemaBuilder(type); | ||
return code `{type:'string'}`; | ||
} | ||
else if (['double', 'number'].some((t) => t === type.type)) { | ||
jsonSchemaBuilder = new NumberJsonSchemaBuilder(type); | ||
return code `{type:'number'}`; | ||
} | ||
else if (type.type === 'boolean') { | ||
jsonSchemaBuilder = new BooleanJsonSchemaBuilder(type); | ||
return code `{type:'boolean'}`; | ||
} | ||
else if (type.type === 'integer') { | ||
jsonSchemaBuilder = new IntegerJsonSchemaBuilder(type); | ||
return code `{type:'integer'}`; | ||
} | ||
else if (type.type === 'not') { | ||
// short-circuit not-any => false | ||
if (type.not.type === 'any') { | ||
return code `false`; | ||
} | ||
return code `{not:${jsonSchemaFor(type.not)}}`; | ||
} | ||
else { | ||
throw new Error(`unsupported type ${type.type}`); | ||
} | ||
return jsonSchemaBuilder.build(); | ||
} | ||
class StringJsonSchemaBuilder { | ||
constructor(type) { | ||
this.type = type; | ||
} | ||
build() { | ||
const result = code `{type:'string'}`; | ||
return result; | ||
} | ||
/** | ||
* Translates a set of Types to the corresponding JSON schemas, removing duplicates. | ||
* | ||
* @param types Types | ||
* @returns JSON schemas for the specified types, deduped | ||
*/ | ||
function schemasFor(...types) { | ||
return Code.join(types.reduce((schemas, type) => { | ||
const schema = jsonSchemaFor(type); | ||
if (!schemas.find((existing) => existing.equals(schema))) { | ||
schemas.push(schema); | ||
} | ||
return schemas; | ||
}, []), ','); | ||
} | ||
class BooleanJsonSchemaBuilder { | ||
constructor(type) { | ||
this.type = type; | ||
} | ||
build() { | ||
const result = code `{type:'boolean'}`; | ||
return result; | ||
} | ||
// constructs the JSON schema for a non-discriminated object type | ||
function buildObjectSchema(type) { | ||
const required = []; | ||
const properties = Object.entries(type.properties).map(([propName, propType]) => { | ||
const safeName = toTypeScriptSafeIdentifier(propName); | ||
if (propType.required) { | ||
required.push(`'${safeName}'`); | ||
} | ||
return code `${safeName}:${jsonSchemaFor(propType.type)}`; | ||
}); | ||
return code `{type:'object',properties:{${Code.join(properties, ',')}},required:[${required.join(',')}],additionalProperties:${jsonSchemaFor(type.additionalProperties)}}`; | ||
} | ||
class NumberJsonSchemaBuilder { | ||
constructor(type) { | ||
this.type = type; | ||
} | ||
build() { | ||
const result = code `{type:'number'}`; | ||
return result; | ||
} | ||
} | ||
class IntegerJsonSchemaBuilder { | ||
constructor(type) { | ||
this.type = type; | ||
} | ||
build() { | ||
const result = code `{type:'integer'}`; | ||
return result; | ||
} | ||
} | ||
class AllOfJsonSchemaBuilder { | ||
constructor(type) { | ||
this.type = type; | ||
} | ||
build() { | ||
const result = code `{`; | ||
const jsonSchemas = this.type.inherits.reduce((schemas, type) => { | ||
// in allOf context, sub types need to relax the additionalProperties member so that | ||
// checks can succeed | ||
const jsonSchema = jsonSchemaFor(type, true); | ||
if (schemas.find((schema) => schema.equals(jsonSchema)) === undefined) { | ||
schemas.push(jsonSchema); | ||
} | ||
return schemas; | ||
}, []); | ||
jsonSchemas.push(jsonSchemaFor(Object.assign({}, this.type, { inherits: [] }), true)); | ||
result.push(code `allOf:[${Code.join(jsonSchemas, ',')}]`); | ||
result.push(`}`); | ||
return result; | ||
} | ||
} | ||
class ObjectJsonSchemaBuilder { | ||
constructor(type, additionalProperties) { | ||
this.type = type; | ||
this.additionalProperties = additionalProperties; | ||
} | ||
build() { | ||
const result = code `{type:'object'`; | ||
const required = []; | ||
result.push(`,properties:{`); | ||
Object.entries(this.type.properties).forEach((entry, i) => { | ||
const [propName, propType] = entry; | ||
if (propType.required) { | ||
required.push(`'${toTypeScriptSafeIdentifier(propName)}'`); | ||
} | ||
result.push(code `${i > 0 ? ',' : ''}${toTypeScriptSafeIdentifier(propName)}:${jsonSchemaFor(propType.type, this.additionalProperties)}`); | ||
}); | ||
result.push(`},required:[${required.join(',')}],additionalProperties:${this.additionalProperties}`); | ||
result.push(`}`); | ||
return result; | ||
} | ||
} | ||
class DiscriminatedObjectJsonSchemaBuilder { | ||
constructor(type, additionalProperties) { | ||
this.type = type; | ||
this.additionalProperties = additionalProperties; | ||
} | ||
build() { | ||
const result = code `{`; | ||
const jsonSchemas = this.type.discriminatorValueMapping | ||
.map((val) => val.type) | ||
.reduce((schemas, type) => { | ||
const jsonSchema = jsonSchemaFor(type, this.additionalProperties); | ||
if (schemas.find((schema) => schema.equals(jsonSchema)) === undefined) { | ||
schemas.push(jsonSchema); | ||
} | ||
return schemas; | ||
}, []); | ||
result.push(code `anyOf:[${Code.join(jsonSchemas, ',')}]`); | ||
result.push(`}`); | ||
return result; | ||
} | ||
} | ||
class OneOfJsonSchemaBuilder { | ||
constructor(type, additionalProperties) { | ||
this.type = type; | ||
this.additionalProperties = additionalProperties; | ||
} | ||
build() { | ||
const result = code `{`; | ||
const jsonSchemas = this.type.anyOf.reduce((schemas, type) => { | ||
const jsonSchema = jsonSchemaFor(type, this.additionalProperties); | ||
if (schemas.find((schema) => schema.equals(jsonSchema)) === undefined) { | ||
schemas.push(jsonSchema); | ||
} | ||
return schemas; | ||
}, []); | ||
result.push(code `anyOf:[${Code.join(jsonSchemas, ',')}]`); | ||
result.push(`}`); | ||
return result; | ||
} | ||
} | ||
class ArrayJsonSchemaBuilder { | ||
constructor(type, additionalProperties) { | ||
this.type = type; | ||
this.additionalProperties = additionalProperties; | ||
} | ||
build() { | ||
const result = code `{type:'array',`; | ||
result.push(code `items:${jsonSchemaFor(this.type.items, this.additionalProperties)}`); | ||
result.push(`}`); | ||
return result; | ||
} | ||
} | ||
@@ -688,2 +609,3 @@ function typeDefinitionFor$1(t, options = {}) { | ||
t.type !== 'nil' && | ||
t.type !== 'not' && | ||
((_b = t.values) === null || _b === void 0 ? void 0 : _b.length) !== undefined) { | ||
@@ -744,2 +666,6 @@ result.push(Code.join(t.values.map((val) => JSON.stringify(val)), '|')); | ||
} | ||
case 'not': { | ||
// no good way to express "not something" in typescript at the moment, defer until someone asks for it | ||
result.push(code `unknown`); | ||
} | ||
} | ||
@@ -757,2 +683,3 @@ } | ||
result.push(...buildPatternProperties(t, options)); | ||
result.push(...buildAdditionalProperties(t, options)); | ||
return result; | ||
@@ -816,2 +743,21 @@ } | ||
} | ||
function buildAdditionalProperties(t, options) { | ||
const result = []; | ||
// No additional properties allowed, default to other parts of the type | ||
if (t.additionalProperties.type === 'not' && t.additionalProperties.not.type === 'any') { | ||
return result; | ||
} | ||
// Additional properties allowed, no schema specified, which means any value is allowed | ||
// but the type is not known | ||
if (t.additionalProperties.type === 'any') { | ||
result.push(code `Record<string,unknown>`); | ||
return result; | ||
} | ||
// Additional properties allowed and we have a schema | ||
result.push(code `Record<string,${typeDefinitionFor$1(t.additionalProperties, { | ||
...options, | ||
useReferences: true, | ||
})}>`); | ||
return result; | ||
} | ||
const MATCH_ALL_PATTERN = '^.*$'; | ||
@@ -943,5 +889,3 @@ function isSupportedPattern(pattern) { | ||
function buildConfigType(params) { | ||
const { inputPayloadDef } = params; | ||
const queryParams = params.queryParams || []; | ||
const otherParams = params.otherParams || []; | ||
const { inputPayloadDef, queryParams = [], otherParams = [], configSchemaType } = params; | ||
const configType = { | ||
@@ -952,2 +896,3 @@ type: 'object', | ||
patternProperties: [], | ||
additionalProperties: { type: 'not', inherits: [], not: { type: 'any', inherits: [] } }, | ||
getDiscriminatedParent: () => undefined, | ||
@@ -957,3 +902,3 @@ discriminator: undefined, | ||
}; | ||
[getQueryParameterMembers(...queryParams), getParameterMembers(...otherParams)].forEach((allParams) => allParams.forEach((param) => { | ||
[...getQueryParameterMembers(...queryParams), ...getParameterMembers(...otherParams)].forEach((param) => { | ||
configType.properties[toCamelCase(param.name)] = { | ||
@@ -963,8 +908,15 @@ type: param.type, | ||
}; | ||
})); | ||
}); | ||
if (inputPayloadDef) { | ||
configType.properties[inputPayloadDef.name] = { | ||
type: inputPayloadDef.type, | ||
required: true, | ||
}; | ||
if (configSchemaType === 'flattened') { | ||
// Input payload needs to be flattened for backwards compatibility with | ||
// older versions of Luvio. | ||
configType.inherits.push(inputPayloadDef.type); | ||
} | ||
else { | ||
configType.properties[inputPayloadDef.name] = { | ||
type: inputPayloadDef.type, | ||
required: true, | ||
}; | ||
} | ||
} | ||
@@ -989,2 +941,45 @@ return configType; | ||
/** | ||
* Validates an API operation against the given endpoint. | ||
* | ||
* This function checks two key aspects of an API operation: | ||
* 1. Whether the `operationId` exists. | ||
* 2. If only one request type is defined for the operation (only one is supported). | ||
* | ||
* @param {Operation} operation - The API operation to be validated. | ||
* @param {EndPoint} endpoint - The endpoint to which the operation belongs. | ||
* | ||
* @throws {Error} If the `operationId` is missing. | ||
* @throws {Error} If more than one request type is found in the operation. | ||
*/ | ||
function validateOperation(operation, endpoint) { | ||
const { operationId } = operation; | ||
if (!operationId) { | ||
throw new Error(`operationId missing from ${operation.method} ${endpoint.path} operation`); | ||
} | ||
if (operation.requests.length > 1) { | ||
throw new Error(`Only 1 request type is currently supported. More than one found in ${operation.method} ${endpoint.path} operation`); | ||
} | ||
} | ||
/** | ||
* Retrieves the default response for a given API operation. | ||
* | ||
* This function searches for the response with status code '200' in the operation's responses. | ||
* If a '200' response is not found, it attempts to find a response with a status code of 'default'. | ||
* | ||
* @param {Operation} operation - The API operation containing the list of responses to search through. | ||
* @param {EndPoint} endpoint - The endpoint to which the operation belongs. Includes information such as the endpoint path. | ||
* | ||
* @returns {Response} The first response found with status code '200' or 'default'. | ||
* | ||
* @throws {Error} If no '200' or 'default' response is found in the operation. | ||
*/ | ||
function getDefaultResponse(operation, endpoint) { | ||
const response = operation.responses.find((resp) => resp.statusCode === '200') || | ||
operation.responses.find((resp) => resp.statusCode === 'default'); | ||
if (!response) { | ||
throw new Error(`missing '200' response in ${operation.method} ${endpoint.path} operation`); | ||
} | ||
return response; | ||
} | ||
/** | ||
* An implementation of OperationCommandService that consumes EndPoint and Operation from an APIService | ||
@@ -1001,5 +996,5 @@ * and generates TypeScript type declarations in Files from a FileService. | ||
const { operationId } = operation; | ||
this.validateOperation(operation, endpoint); | ||
validateOperation(operation, endpoint); | ||
const request = operation.requests[0]; | ||
const response = this.getDefaultResponse(operation, endpoint); | ||
const response = getDefaultResponse(operation, endpoint); | ||
const server = this.getServer(endpoint, operation); | ||
@@ -1029,2 +1024,7 @@ const commandName = `${toTypeScriptSafeIdentifier(operationId)}Command`; | ||
}, code ``); | ||
const processedUrl = this.processUrl({ | ||
...params, | ||
request, | ||
server, | ||
}); | ||
file.push(code `export const ${schemaConfigConst}: ${JSON_SCHEMA} = ${configJsonSchema};`, code `type BaseType = ${this.commandBaseClass}<${typeDefinitionFor(this.services, outputType)}, ${commandNamedServices}>;`, code `type BaseConstructorType = typeof ${this.commandBaseClass}<${typeDefinitionFor(this.services, outputType)},${commandNamedServices}>;`, code `type BaseConstructorParams = ConstructorParameters<BaseConstructorType>;`, code `type ${commandClassTypeName} = new (config: ${commandConfigType}, ...args: BaseConstructorParams) => BaseType;`, code `export function ${builderFunctionName}(baseClass: BaseConstructorType): ${commandClassTypeName}{`, code `return class ${className} extends baseClass{`, code `constructor(private config: ${commandConfigType}, ...args: BaseConstructorParams){super(...args)}`, this.generateClassBody({ | ||
@@ -1036,2 +1036,3 @@ ...params, | ||
outputType, | ||
processedUrl, | ||
}), '}};'); | ||
@@ -1057,19 +1058,2 @@ return { | ||
} | ||
validateOperation(operation, endpoint) { | ||
const { operationId } = operation; | ||
if (!operationId) { | ||
throw new Error(`operationId missing from ${operation.method} ${endpoint.path} operation`); | ||
} | ||
if (operation.requests.length > 1) { | ||
throw new Error(`Only 1 request type is currently supported. More than one found in ${operation.method} ${endpoint.path} operation`); | ||
} | ||
} | ||
getDefaultResponse(operation, endpoint) { | ||
const response = operation.responses.find((resp) => resp.statusCode === '200') || | ||
operation.responses.find((resp) => resp.statusCode === 'default'); | ||
if (!response) { | ||
throw new Error(`missing '200' response in ${operation.method} ${endpoint.path} operation`); | ||
} | ||
return response; | ||
} | ||
getServer(endpoint, operation) { | ||
@@ -1115,2 +1099,3 @@ // TODO - picking first server is probably not always correct | ||
], | ||
configSchemaType: operation.configSchemaType, | ||
}); | ||
@@ -1128,9 +1113,9 @@ return configType; | ||
return isNumericType(type) | ||
? `this.config.${name}` | ||
? `${name}` | ||
: isStringType(type) | ||
? `encodeURIComponent(this.config.${name})` | ||
? `encodeURIComponent(${name})` | ||
: isArrayType(type) | ||
? type.items !== undefined && isStringType(type.items) | ||
? `this.config.${name}.map(item=>encodeURIComponent(item)).join(',')` | ||
: `this.config.${name}.join(',')` | ||
? `${name}.map(item=>encodeURIComponent(item)).join(',')` | ||
: `${name}.join(',')` | ||
: undefined; | ||
@@ -1178,3 +1163,6 @@ } | ||
super(...arguments); | ||
this.serviceDependencies = [ | ||
this.commandBaseClass = AURA_COMMAND_BASE_CLASS; | ||
} | ||
get serviceDependencies() { | ||
return [ | ||
{ | ||
@@ -1187,3 +1175,2 @@ version: '1.0', | ||
]; | ||
this.commandBaseClass = AURA_COMMAND_BASE_CLASS; | ||
} | ||
@@ -1199,36 +1186,57 @@ getInputPayloadParameterName(config) { | ||
generateClassBody(config) { | ||
var _a; | ||
const { endpoint, operation, request, server, hasInputPayload } = config; | ||
const auraControllerName = endpoint.auraController.name; | ||
if (auraControllerName === undefined) { | ||
// controller name is mandatory | ||
throw new Error(`Missing Connect API family name or Aura controller name in extensions for ${operation.method} ${endpoint.path} operation`); | ||
const { endpoint, operation } = config; | ||
// I would prefer to organize this so this code becomes iterating through keys, and calling generators | ||
return code `${this.generateAuraEndpoint(endpoint, operation)}${this.generateAuraParamsCode(config)}${this.generateAuraActionConfig()}`; | ||
} | ||
// TODO: Need to actually support overriding pieces of the actionConfig using annotations or the incoming config | ||
generateAuraActionConfig() { | ||
return code ``; // Default values are provided in AuraNetworkCommand class | ||
} | ||
generateAuraParamsCode(config) { | ||
const { request, server, endpoint, hasInputPayload } = config; | ||
const result = code `get auraParams():Record<string,unknown>{`; | ||
// handle uri params, query params and input payload | ||
const queryParameters = getQueryParameterMembers(request.queryParameters); | ||
const uriParameters = getParameterMembers(server.uriParameters, endpoint.uriParameters, request.uriParameters); | ||
const allParams = [...queryParameters, ...uriParameters] | ||
.map(({ name }) => toCamelCase(name)) | ||
.join(','); | ||
const inputPayloadDelimiter = allParams.length && hasInputPayload ? ',' : ''; | ||
if (allParams.length || hasInputPayload) { | ||
// Create destructing assignment to be returned | ||
// ex: const { param1, param2, ...body } = this.config; | ||
result.push(code `const { | ||
${allParams} | ||
${inputPayloadDelimiter} | ||
${this.generateInputPayloadParameter(config)} | ||
} = this.config;`); | ||
} | ||
// make sure all parameters part of server+url specification are available, even if we don't use the | ||
// processed url directly, these will be needed on Aura controller side | ||
this.processUrl(config); | ||
// generate endpoint | ||
// the extensions member has been initialized as part of the processExtensions call | ||
const auraMethodName = ((_a = operation.auraMethod) === null || _a === void 0 ? void 0 : _a.name) || operation.operationId; | ||
const result = code ``; | ||
result.push(code `endpoint = '${auraControllerName}.${auraMethodName}';`); | ||
result.push(code `get auraParams(): Record<string, unknown>{`); | ||
// handle uri params, query params and input payload | ||
const queryParameters = getQueryParameterMembers(request === null || request === void 0 ? void 0 : request.queryParameters); | ||
const uriParameters = getParameterMembers(server.uriParameters, endpoint.uriParameters, request === null || request === void 0 ? void 0 : request.uriParameters); | ||
result.push(code `return {`); | ||
[queryParameters, uriParameters].forEach((allParams) => { | ||
allParams.forEach((param) => { | ||
result.push(code `'${param.name}'${param.required ? '' : '?'}:this.config.${toCamelCase(param.name)},`); | ||
}); | ||
}); | ||
result.push(code `return { ${allParams}`); | ||
if (hasInputPayload) { | ||
// the extensions member has been initialized as part of the processExtensions call | ||
const inputPayload = this.getInputPayloadParameterName(config); | ||
result.push(code `${inputPayload}:this.config.${inputPayload},`); | ||
result.push(code `${inputPayloadDelimiter}${inputPayload}:body`); | ||
} | ||
result.push(`};`); | ||
result.push(code `}`); | ||
result.push(`};}`); | ||
return result; | ||
} | ||
generateInputPayloadParameter(config) { | ||
const { hasInputPayload, operation: { configSchemaType }, } = config; | ||
if (hasInputPayload) { | ||
if (configSchemaType === 'flattened') { | ||
// Use rest operator when flattening, | ||
// e.g. const { ...body } = this.config | ||
return code `...body`; | ||
} | ||
else { | ||
// Extract using input payload name otherwise, | ||
// e.g. const { inputPayload: body } = this.config | ||
return code `${this.getInputPayloadParameterName(config)}: body`; | ||
} | ||
} | ||
} | ||
generateAuraEndpoint(endpoint, operation) { | ||
const auraControllerName = endpoint.auraController.name; | ||
return code `endpoint='${auraControllerName}.${operation.auraMethod.name}';`; | ||
} | ||
} | ||
@@ -1273,5 +1281,4 @@ | ||
} | ||
generateUrl(config) { | ||
const url = this.processUrl(config); | ||
return code `let url:Parameters<${FETCH_SERVICE}>[0]=\`${url}\`;`; | ||
generateUrl(processedUrl) { | ||
return code `let url:Parameters<${FETCH_SERVICE}>[0]=\`${processedUrl}\`;`; | ||
} | ||
@@ -1283,2 +1290,3 @@ generateHeaders(config) { | ||
let result = undefined; | ||
let names = []; | ||
if (hasHeaders) { | ||
@@ -1293,3 +1301,3 @@ // TODO - resolve cookies? | ||
.filter((hdr) => hdr.required) | ||
.forEach((hdr) => result.push(`'${hdr.name}':this.config.${toCamelCase(hdr.name)},`)); | ||
.forEach((hdr) => result.push(`'${hdr.name}':${toCamelCase(hdr.name)},`)); | ||
result.push(`};`); | ||
@@ -1300,6 +1308,7 @@ headers | ||
const headerName = toCamelCase(hdr.name); | ||
result.push(`if(this.config.${headerName}!==undefined){headers['${hdr.name}']=this.config.${headerName};}`); | ||
result.push(`if(${headerName}!==undefined){headers['${hdr.name}']=${headerName};}`); | ||
}); | ||
names = headers.map((header) => toCamelCase(header.name)); | ||
} | ||
return result; | ||
return { names, code: result }; | ||
} | ||
@@ -1310,2 +1319,3 @@ generateQueryParams(config) { | ||
let result = undefined; | ||
let names = []; | ||
const queryParameters = getQueryParameterMembers(request === null || request === void 0 ? void 0 : request.queryParameters); | ||
@@ -1320,12 +1330,48 @@ if (queryParameters.length > 0) { | ||
} | ||
result.push(param.required ? '' : `if(this.config.${paramName}!==undefined){`, `queryParams.push('${param.name}='+${value});`, param.required ? '' : `}`); | ||
result.push(param.required ? '' : `if(${paramName}!==undefined){`, `queryParams.push('${param.name}='+${value});`, param.required ? '' : `}`); | ||
names.push(paramName); | ||
}); | ||
result.push(`if(queryParams.length>0){url+='?'+queryParams.join('&');}`); | ||
} | ||
return { names, code: result }; | ||
} | ||
generateAllParams(headers, queryParameters, config) { | ||
const { hasInputPayload, request, server, endpoint } = config; | ||
const uriParameters = getParameterMembers(server.uriParameters, endpoint.uriParameters, request === null || request === void 0 ? void 0 : request.uriParameters); | ||
const result = new Code(); | ||
const allParams = [ | ||
...headers, | ||
...queryParameters, | ||
...uriParameters.map((param) => toCamelCase(param.name)), | ||
]; | ||
if (hasInputPayload || allParams.length) { | ||
result.push(code `const { | ||
${allParams.join(',')} | ||
${allParams.length && hasInputPayload ? ',' : ''} | ||
${this.generateInputPayloadParameter(config)} | ||
} = this.config;`); | ||
} | ||
return result; | ||
} | ||
generateClassBody(config) { | ||
const { request, operation, hasInputPayload } = config; | ||
const result = new Code().push(code `get fetchParams(): Parameters<${FETCH_SERVICE}>{`, code `${this.generateUrl(config)}${this.generateQueryParams(config)}${this.generateHeaders(config)}`); | ||
const hasHeaders = getParameterMembers(request === null || request === void 0 ? void 0 : request.headers).length > 0 || hasInputPayload; | ||
generateInputPayloadParameter(config) { | ||
const { hasInputPayload, operation: { configSchemaType }, } = config; | ||
if (hasInputPayload) { | ||
if (configSchemaType === 'flattened') { | ||
// Use rest operator when flattening, | ||
// e.g. const { ...body } = this.config | ||
return code `...body`; | ||
} | ||
else { | ||
// Extract using input payload name otherwise, | ||
// e.g. const { inputPayload: body } = this.config | ||
return code `${this.getInputPayloadParameterName(config)}: body`; | ||
} | ||
} | ||
} | ||
generateFetchParams(config) { | ||
const { operation, hasInputPayload, processedUrl } = config; | ||
const headers = this.generateHeaders(config); | ||
const queryParams = this.generateQueryParams(config); | ||
const result = new Code().push(code `get fetchParams(): Parameters<${FETCH_SERVICE}>{`, code `${this.generateAllParams(headers.names, queryParams.names, config)}`, code `${this.generateUrl(processedUrl)}${queryParams.code}${headers.code}`); | ||
const hasHeaders = headers.names.length > 0 || hasInputPayload; | ||
result.push(code `const params:Parameters<${FETCH_SERVICE}>[1]={`, code `method:'${operation.method.toUpperCase()}',`, code `cache:'no-cache',`); | ||
@@ -1336,3 +1382,3 @@ if (hasHeaders) { | ||
if (hasInputPayload) { | ||
result.push(`,body:JSON.stringify(this.config.${INPUT_PAYLOAD_MEMBER_NAME})`); | ||
result.push(`,body:JSON.stringify(body)`); | ||
} | ||
@@ -1343,4 +1389,47 @@ result.push(`};`); | ||
} | ||
generateClassBody(config) { | ||
return code `${this.generateFetchParams(config)}`; | ||
} | ||
} | ||
const AURA_CACHE_CONTROL_COMMAND_BASE_CLASS = { | ||
module: '@luvio/command-aura-resource-cache-control/v1', | ||
exportedSymbol: 'AuraCacheControlCommand', | ||
isType: true, | ||
}; | ||
const NAMED_CACHE_CONTROL_SERVICE = { | ||
module: '@luvio/service-cache-control/v1', | ||
exportedSymbol: 'NamedCacheControlService', | ||
isType: true, | ||
}; | ||
const CACHE_CONTROL_SERVICE_DESCRIPTOR = { | ||
module: '@luvio/service-cache-control/v1', | ||
exportedSymbol: 'CacheControlServiceDescriptor', | ||
isType: true, | ||
}; | ||
class AuraResourceCacheControlCommandGenerator extends AuraCommandGenerator { | ||
constructor() { | ||
super(...arguments); | ||
this.commandBaseClass = AURA_CACHE_CONTROL_COMMAND_BASE_CLASS; | ||
} | ||
get serviceDependencies() { | ||
return [ | ||
...super.serviceDependencies, | ||
{ | ||
version: '1.0', | ||
descriptor: CACHE_CONTROL_SERVICE_DESCRIPTOR, | ||
namedService: NAMED_CACHE_CONTROL_SERVICE, | ||
name: 'cacheControl', | ||
}, | ||
]; | ||
} | ||
generateBuildCacheControlMetadata(operation) { | ||
return code `buildCacheControlMetadata(_networkResult:NetworkData){return{type:'max-age',maxAge:${operation.cacheStrategy.config.maxAge},generatedTime:Date.now()/1000}as const;}`; | ||
} | ||
generateClassBody(config) { | ||
const { operation } = config; | ||
return code `${super.generateClassBody(config)}${this.generateBuildCacheControlMetadata(operation)}`; | ||
} | ||
} | ||
function operationCommandGeneratorService(services) { | ||
@@ -1350,3 +1439,7 @@ let commands = []; | ||
for (const operation of endpoint.operations) { | ||
const commandGenerator = new (endpoint.type === 'aura' ? AuraCommandGenerator : HttpCommandGenerator)(services); | ||
const commandGenerator = new (endpoint.type === 'aura' | ||
? operation.cacheStrategy.type === 'none' | ||
? AuraCommandGenerator | ||
: AuraResourceCacheControlCommandGenerator | ||
: HttpCommandGenerator)(services); | ||
commands.push({ | ||
@@ -1353,0 +1446,0 @@ ...commandGenerator.build({ endpoint, operation }), |
@@ -5,3 +5,4 @@ import { CommandGenerator } from './command-generator'; | ||
export declare class AuraCommandGenerator extends CommandGenerator { | ||
serviceDependencies: { | ||
commandBaseClass: ImportableReference; | ||
get serviceDependencies(): { | ||
version: "1.0"; | ||
@@ -12,3 +13,2 @@ descriptor: ImportableReference; | ||
}[]; | ||
commandBaseClass: ImportableReference; | ||
getInputPayloadParameterName(config: { | ||
@@ -25,3 +25,19 @@ endpoint: EndPoint; | ||
outputType: Type | undefined; | ||
processedUrl: string; | ||
}): Code; | ||
generateAuraActionConfig(): Code; | ||
generateAuraParamsCode(config: { | ||
endpoint: AuraEndPoint; | ||
operation: AuraOperation; | ||
request: Request; | ||
server: Server; | ||
hasInputPayload: boolean; | ||
outputType: Type | undefined; | ||
}): Code; | ||
generateInputPayloadParameter(config: { | ||
hasInputPayload: boolean; | ||
endpoint: EndPoint; | ||
operation: AuraMutationOperation; | ||
}): Code | undefined; | ||
generateAuraEndpoint(endpoint: AuraEndPoint, operation: AuraOperation): Code; | ||
} |
import type { NamedLoggerService, ServiceVersion } from '@luvio/utils'; | ||
import type { EndPoint, ObjectType, Operation, Parameter, Payload, Request, Server, Type, QueryParameter, TypeOfParameter } from '@luvio/model'; | ||
import type { EndPoint, ObjectType, Operation, Parameter, Payload, Request, Server, Type, QueryParameter, TypeOfParameter, ConfigSchemaType } from '@luvio/model'; | ||
import type { NamedAPIService } from '../api'; | ||
@@ -72,2 +72,3 @@ import type { ImportableReference, Code, NamedFileService } from '../files'; | ||
otherParams?: Record<string, Parameter>[]; | ||
configSchemaType?: ConfigSchemaType; | ||
}): ObjectType; | ||
@@ -83,2 +84,30 @@ /** | ||
/** | ||
* Validates an API operation against the given endpoint. | ||
* | ||
* This function checks two key aspects of an API operation: | ||
* 1. Whether the `operationId` exists. | ||
* 2. If only one request type is defined for the operation (only one is supported). | ||
* | ||
* @param {Operation} operation - The API operation to be validated. | ||
* @param {EndPoint} endpoint - The endpoint to which the operation belongs. | ||
* | ||
* @throws {Error} If the `operationId` is missing. | ||
* @throws {Error} If more than one request type is found in the operation. | ||
*/ | ||
export declare function validateOperation(operation: Operation, endpoint: EndPoint): void; | ||
/** | ||
* Retrieves the default response for a given API operation. | ||
* | ||
* This function searches for the response with status code '200' in the operation's responses. | ||
* If a '200' response is not found, it attempts to find a response with a status code of 'default'. | ||
* | ||
* @param {Operation} operation - The API operation containing the list of responses to search through. | ||
* @param {EndPoint} endpoint - The endpoint to which the operation belongs. Includes information such as the endpoint path. | ||
* | ||
* @returns {Response} The first response found with status code '200' or 'default'. | ||
* | ||
* @throws {Error} If no '200' or 'default' response is found in the operation. | ||
*/ | ||
export declare function getDefaultResponse(operation: Operation, endpoint: EndPoint): import("@luvio/model").Response<Type>; | ||
/** | ||
* An implementation of OperationCommandService that consumes EndPoint and Operation from an APIService | ||
@@ -94,4 +123,2 @@ * and generates TypeScript type declarations in Files from a FileService. | ||
}): CommandInfo; | ||
private validateOperation; | ||
private getDefaultResponse; | ||
abstract getInputPayloadParameterName(config: { | ||
@@ -108,2 +135,3 @@ endpoint: EndPoint; | ||
outputType: Type | undefined; | ||
processedUrl: string; | ||
}): Code; | ||
@@ -110,0 +138,0 @@ abstract get serviceDependencies(): ServiceInfo[]; |
@@ -17,19 +17,41 @@ import { Code } from '../files'; | ||
commandBaseClass: ImportableReference; | ||
generateUrl(config: { | ||
generateUrl(processedUrl: string): Code; | ||
generateHeaders(config: { | ||
endpoint: EndPoint; | ||
operation: Operation; | ||
request: Request; | ||
hasInputPayload: boolean; | ||
}): { | ||
names: string[]; | ||
code: Code | undefined; | ||
}; | ||
generateQueryParams(config: { | ||
endpoint: EndPoint; | ||
operation: Operation; | ||
request: Request; | ||
}): { | ||
names: string[]; | ||
code: Code | undefined; | ||
}; | ||
generateAllParams(headers: string[], queryParameters: string[], config: { | ||
endpoint: EndPoint; | ||
operation: Operation; | ||
request: Request; | ||
server: Server; | ||
hasInputPayload: boolean; | ||
}): Code; | ||
generateHeaders(config: { | ||
generateInputPayloadParameter(config: { | ||
hasInputPayload: boolean; | ||
endpoint: EndPoint; | ||
operation: Operation; | ||
request: Request; | ||
hasInputPayload: boolean; | ||
}): Code | undefined; | ||
generateQueryParams(config: { | ||
generateFetchParams(config: { | ||
endpoint: EndPoint; | ||
operation: Operation; | ||
request: Request; | ||
}): Code | undefined; | ||
server: Server; | ||
hasInputPayload: boolean; | ||
outputType: Type | undefined; | ||
processedUrl: string; | ||
}): Code; | ||
generateClassBody(config: { | ||
@@ -42,3 +64,4 @@ endpoint: EndPoint; | ||
outputType: Type | undefined; | ||
processedUrl: string; | ||
}): Code; | ||
} |
@@ -47,4 +47,4 @@ export type ImportableReference = ({ | ||
* | ||
* @param newChunks | ||
* @returns | ||
* @param newChunks code to be appended | ||
* @returns this | ||
*/ | ||
@@ -60,2 +60,14 @@ push(...newChunks: any[]): this; | ||
equals(comparison: Code): boolean; | ||
/** | ||
* Returns the Code as a single string. | ||
* | ||
* @param options | ||
* importStringifier - function to convert ImportableReferences to strings. | ||
* Defaults to an internal function whose only guarantee is that equivalent | ||
* ImportableReferencesit will result in equivalent strings. | ||
* @returns string representation of the code | ||
*/ | ||
toString(options?: { | ||
importStringifier?: (reference: ImportableReference) => string; | ||
}): string; | ||
} | ||
@@ -62,0 +74,0 @@ /** |
@@ -29,6 +29,15 @@ import { Code } from './code'; | ||
*/ | ||
export declare class File extends Code { | ||
export declare class File { | ||
readonly filename: string; | ||
code: Code; | ||
constructor(filename: string); | ||
/** | ||
* Appends new chunks of code to this list. Parameters are handled as in | ||
* Code.push(). | ||
* | ||
* @param newChunks code to be appended | ||
* @returns this | ||
*/ | ||
push(...newChunks: any[]): this; | ||
/** | ||
* Returns the code for this File, with ImportableReferences resolved. | ||
@@ -35,0 +44,0 @@ * |
@@ -0,3 +1,9 @@ | ||
import { type Type } from '@luvio/model'; | ||
import { Code } from '../files'; | ||
import type { Type } from '@luvio/model'; | ||
export declare function jsonSchemaFor(type: Type, additionalProperties?: boolean): Code; | ||
/** | ||
* Returns the JSON schema for a specified type. | ||
* | ||
* @param type the type | ||
* @returns JSON schema for type | ||
*/ | ||
export declare function jsonSchemaFor(type: Type): Code; |
{ | ||
"name": "@luvio/generator-ts", | ||
"version": "5.11.0", | ||
"version": "5.12.0", | ||
"description": "Luvio TypeScript code generation", | ||
@@ -25,10 +25,10 @@ "repository": { | ||
"dependencies": { | ||
"@luvio/model": "5.11.0", | ||
"@luvio/model": "5.12.0", | ||
"prettier": "^2.7.1" | ||
}, | ||
"devDependencies": { | ||
"@luvio/service-aura-network": "5.11.0", | ||
"@luvio/service-broker": "5.11.0", | ||
"@luvio/service-fetch-network": "5.11.0", | ||
"@luvio/utils": "5.11.0", | ||
"@luvio/service-aura-network": "5.12.0", | ||
"@luvio/service-broker": "5.12.0", | ||
"@luvio/service-fetch-network": "5.12.0", | ||
"@luvio/utils": "5.12.0", | ||
"memfs": "^3.4.13" | ||
@@ -35,0 +35,0 @@ }, |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
330092
49
2533
+ Added@luvio/model@5.12.0(transitive)
- Removed@luvio/model@5.11.0(transitive)
Updated@luvio/model@5.12.0