@autobe/agent
Version:
AI backend server code generator
586 lines (585 loc) • 31.1 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AutoBeJsonSchemaFactory = void 0;
const utils_1 = require("@autobe/utils");
const utils_2 = require("@typia/utils");
const typia_1 = __importDefault(require("typia"));
const uuid_1 = require("uuid");
const AutoBeInterfaceSchemaProgrammer_1 = require("../programmers/AutoBeInterfaceSchemaProgrammer");
const AutoBeJsonSchemaValidator_1 = require("./AutoBeJsonSchemaValidator");
var AutoBeJsonSchemaFactory;
(function (AutoBeJsonSchemaFactory) {
/* -----------------------------------------------------------
ASSIGNMENTS
----------------------------------------------------------- */
AutoBeJsonSchemaFactory.presets = (typeNames) => {
const schemas = {};
for (const [key, value] of Object.entries(AutoBeJsonSchemaFactory.DEFAULT_SCHEMAS)) {
schemas[key] = value;
typeNames.delete(key);
}
for (const key of typeNames)
if (AutoBeJsonSchemaValidator_1.AutoBeJsonSchemaValidator.isPage(key)) {
const data = AutoBeJsonSchemaFactory.getPageName(key);
schemas[key] = AutoBeJsonSchemaFactory.writePageSchema(data);
typeNames.delete(key);
typeNames.add(data);
}
return schemas;
};
AutoBeJsonSchemaFactory.fixPaginationSchemas = (schemas) => {
const pageRequest = AutoBeJsonSchemaFactory.DEFAULT_SCHEMAS["IPage.IRequest"];
for (const [key, value] of Object.entries(schemas)) {
if (key.endsWith(".IRequest") === false)
continue;
else if (utils_1.AutoBeOpenApiTypeChecker.isObject(value) === false)
continue;
if (value.properties.page === undefined)
value.properties.page = pageRequest.properties.page;
if (value.properties.limit === undefined)
value.properties.limit = pageRequest.properties.limit;
}
// Rewrite every $ref pointing to a bogus .IPagination variant
// (e.g. IEcommerceMall.IPagination) → IPage.IPagination.
for (const value of Object.values(schemas))
utils_1.AutoBeOpenApiTypeChecker.skim({
schema: value,
accessor: "",
closure: (next) => {
if (utils_1.AutoBeOpenApiTypeChecker.isReference(next) &&
next.$ref.endsWith(".IPagination") &&
next.$ref !== "#/components/schemas/IPage.IPagination")
next.$ref = "#/components/schemas/IPage.IPagination";
},
});
// Delete the bogus schemas themselves so the LLM never sees them
// in subsequent iterations. Covers both entity variants
// (IEcommerceMall.IPagination) and their page wrappers
// (IPageIEcommerceMall.IPagination).
for (const key of Object.keys(schemas))
if (key.endsWith(".IPagination") && key !== "IPage.IPagination")
delete schemas[key];
};
AutoBeJsonSchemaFactory.fixAuthorizationSchemas = (schemas) => {
for (const [key, value] of Object.entries(schemas)) {
if (key.endsWith(".IAuthorized") === false)
continue;
else if (utils_1.AutoBeOpenApiTypeChecker.isObject(value) === false)
continue;
const parent = schemas[key.replace(".IAuthorized", "")];
if (parent === undefined ||
utils_1.AutoBeOpenApiTypeChecker.isObject(parent) === false) {
value.properties.token = {
"x-autobe-specification": "Authorization token comes from the session table.",
description: "Authorization token.",
$ref: "#/components/schemas/IAuthorizationToken",
};
if (value.required.includes("token") === false)
value.required.push("token");
}
else {
value.properties = Object.assign(Object.assign({}, parent.properties), value.properties);
value.properties.token = {
"x-autobe-specification": "Authorization token comes from the session table.",
description: "Authorization token.",
$ref: "#/components/schemas/IAuthorizationToken",
};
value.required = Array.from(new Set([...parent.required, ...value.required]));
if (value.required.includes("id") === false)
value.required.push("id");
if (value.required.includes("token") === false)
value.required.push("token");
}
}
};
AutoBeJsonSchemaFactory.finalize = (props) => {
removeDuplicated(props);
fixTimestamps({
application: props.application,
document: {
operations: props.operations,
components: {
schemas: props.collection.schemas,
authorizations: [],
},
},
});
linkRelatedModels({
application: props.application,
document: {
operations: props.operations,
components: {
schemas: props.collection.schemas,
authorizations: [],
},
},
});
};
AutoBeJsonSchemaFactory.removeUnused = (props) => {
while (true) {
const used = new Set();
const visit = (schema) => utils_2.OpenApiTypeChecker.visit({
components: { schemas: props.schemas },
schema,
closure: (next) => {
if (utils_2.OpenApiTypeChecker.isReference(next)) {
const key = next.$ref.split("/").pop();
used.add(key);
}
},
});
for (const op of props.operations) {
if (op.requestBody !== null)
visit({
$ref: `#/components/schemas/${op.requestBody.typeName}`,
});
if (op.responseBody !== null)
visit({
$ref: `#/components/schemas/${op.responseBody.typeName}`,
});
}
const complete = Object.keys(props.schemas).length === 0 ||
Object.keys(props.schemas).every((key) => used.has(key) === true);
if (complete === true)
break;
for (const key of Object.keys(props.schemas))
if (used.has(key) === false)
delete props.schemas[key];
}
};
const removeDuplicated = (props) => {
// gather duplicated schemas
const correct = new Map();
for (const key of Object.keys(props.collection.schemas)) {
if (key.includes(".") === false)
continue;
const dotRemoved = key.replace(".", "");
if (props.collection.schemas[dotRemoved] === undefined)
continue;
correct.set(dotRemoved, key);
}
// fix operations' references
for (const op of props.operations) {
if (op.requestBody && correct.has(op.requestBody.typeName))
op.requestBody.typeName = correct.get(op.requestBody.typeName);
if (op.responseBody && correct.has(op.responseBody.typeName))
op.responseBody.typeName = correct.get(op.responseBody.typeName);
}
// fix schemas' references
const $refChangers = new Map();
for (const value of Object.values(props.collection.schemas))
utils_2.OpenApiTypeChecker.visit({
components: { schemas: props.collection.schemas },
schema: value,
closure: (next) => {
if (utils_2.OpenApiTypeChecker.isReference(next) === false)
return;
const x = next.$ref.split("/").pop();
const y = correct.get(x);
if (y === undefined)
return;
$refChangers.set(next, () => (next.$ref = `#/components/schemas/${y}`));
},
});
for (const fn of $refChangers.values())
fn();
// remove duplicated schemas
for (const key of correct.keys())
props.collection.delete(key);
};
const fixTimestamps = (props) => {
const entireModels = props.application.files
.map((f) => f.models)
.flat();
for (const value of Object.values(props.document.components.schemas)) {
if (utils_1.AutoBeOpenApiTypeChecker.isObject(value) === false)
continue;
const model = value["x-autobe-database-schema"]
? entireModels.find((m) => m.name === value["x-autobe-database-schema"])
: undefined;
if (model === undefined)
continue;
const properties = Object.keys(value.properties);
for (const key of properties) {
if (key !== "created_at" &&
key !== "updated_at" &&
key !== "deleted_at")
continue;
const column = model.plainFields.find((c) => c.name === key);
if (column === undefined)
delete value.properties[key];
}
}
};
const linkRelatedModels = (props) => {
var _a;
const modelDict = new Set(props.application.files
.map((f) => f.models)
.flat()
.map((m) => m.name));
for (const [key, value] of Object.entries(props.document.components.schemas)) {
if (utils_1.AutoBeOpenApiTypeChecker.isObject(value) === false ||
!!((_a = value["x-autobe-database-schema"]) === null || _a === void 0 ? void 0 : _a.length))
continue;
const typeName = key.split(".")[0].substring(1);
const modelName = AutoBeInterfaceSchemaProgrammer_1.AutoBeInterfaceSchemaProgrammer.getDatabaseSchemaName(typeName);
if (modelDict.has(modelName) === true)
value["x-autobe-database-schema"] = modelName;
}
};
/* -----------------------------------------------------------
PAGINATION
----------------------------------------------------------- */
AutoBeJsonSchemaFactory.writePageSchema = (key) => ({
type: "object",
properties: {
pagination: {
"x-autobe-specification": "Pagination information for the page.",
description: "Page information.",
$ref: "#/components/schemas/IPage.IPagination",
},
data: {
"x-autobe-specification": `List of records of type ${key}.`,
description: "List of records.",
type: "array",
items: {
$ref: `#/components/schemas/${key}`,
},
},
},
required: ["pagination", "data"],
description: utils_1.StringUtil.trim `
A page.
Collection of records with pagination information.
`,
"x-autobe-specification": `A page containing records of type ${key}.`,
"x-autobe-database-schema": null, // filled by relation review agent
});
// export const fixPage = (path: string, input: unknown): void => {
// if (isRecord(input) === false || isRecord(input[path]) === false) return;
// if (input[path].description) delete input[path].description;
// if (input[path].required) delete input[path].required;
// for (const key of Object.keys(input[path]))
// if (DEFAULT_SCHEMAS[key] !== undefined)
// input[path][key] = DEFAULT_SCHEMAS[key];
// else if (AutoBeJsonSchemaValidator.isPage(key) === true) {
// const data: string = key.substring("IPage".length);
// input[path][key] = writePageSchema(data);
// }
// };
AutoBeJsonSchemaFactory.getPageName = (key) => key.substring("IPage".length);
// const isRecord = (input: unknown): input is Record<string, unknown> =>
// typeof input === "object" && input !== null;
AutoBeJsonSchemaFactory.DEFAULT_SCHEMAS = (() => {
var _a, _b;
const init = ((_b = (_a = {
version: "3.1",
components: {
schemas: {
"IPage.IPagination": {
type: "object",
properties: {
current: {
type: "integer",
minimum: 0,
description: "Current page number being viewed (1-indexed).\n\nIndicates which page of results is currently being returned. Page\nnumbering starts from 1, so the first page is page 1 (not 0). This value\nreflects the page parameter from the request after validation and bounds\nchecking.",
"x-autobe-specification": "1-indexed current page number. Defaults to 1."
},
limit: {
type: "integer",
minimum: 0,
description: "Maximum number of records per page.\n\nDefines the upper bound on how many records can be returned in a single\npage. This corresponds to the limit parameter from the request. The\nactual number of records in the data array may be less than this value on\nthe final page or when total records are fewer than the limit.",
"x-autobe-specification": "Maximum records per page. Actual count may be less on last page."
},
records: {
type: "integer",
minimum: 0,
description: "Total count of all records matching the query criteria.\n\nRepresents the complete number of records available across all pages, not\njust the current page. This value is computed via a COUNT query and is\nessential for calculating total pages and displaying pagination UI\nelements like \"Showing 1-10 of 150 results\".",
"x-autobe-specification": "Total record count across all pages."
},
pages: {
type: "integer",
minimum: 0,
description: "Total number of pages available.\n\nCalculated as ceiling of {@link records} divided by {@link limit}. When\nrecords is 0, pages will also be 0. This value enables clients to render\npage navigation controls and validate page bounds.",
"x-autobe-specification": "Total pages. Calculated as Math.ceil(records / limit)."
}
},
required: [
"current",
"limit",
"records",
"pages"
],
description: "Pagination metadata containing current page position and total data\nstatistics.\n\nThis interface provides comprehensive pagination information returned\nalongside paginated list data. It enables clients to implement navigation\ncontrols, display progress indicators, and determine data boundaries for UI\nrendering.",
"x-autobe-specification": "Pagination metadata for paginated list responses. Included in all list endpoint responses."
},
"IPage.IRequest": {
type: "object",
properties: {
page: {
oneOf: [
{
type: "null"
},
{
type: "integer",
minimum: 0
}
],
description: "Target page number to retrieve (1-indexed).\n\nSpecifies which page of results to return. Page numbering starts from 1.\nIf omitted, null, or undefined, defaults to page 1 (first page).\nRequesting a page beyond the available range returns an empty data array\nwith valid pagination metadata reflecting the actual totals.",
"x-autobe-specification": "1-indexed page number. Defaults to 1 if not provided."
},
limit: {
oneOf: [
{
type: "null"
},
{
type: "integer",
minimum: 0
}
],
description: "Maximum number of records to return per page.\n\nControls how many records are included in each page response. If omitted,\nnull, or undefined, defaults to 100 records per page. The server may\nenforce upper bounds to prevent excessive resource consumption on large\nrequests.",
"x-autobe-specification": "Maximum records per page. Defaults to 100 if not provided."
}
},
required: [],
description: "Pagination request parameters for list endpoints.\n\nDefines the query parameters used to control pagination when requesting\nlist data. Both parameters are optional with sensible defaults, allowing\nclients to fetch data without specifying pagination if default behavior is\nacceptable.",
"x-autobe-specification": "Pagination query parameters for list endpoints. All fields optional."
},
IAuthorizationToken: {
type: "object",
properties: {
access: {
type: "string",
description: "Short-lived JWT access token for authenticating API requests.\n\nThis token must be included in the Authorization header using the Bearer\nscheme (e.g., `Authorization: Bearer {access}`) for all endpoints requiring\nauthentication. The token contains encoded claims including user identity,\nroles, and permissions. Typically expires within 15-60 minutes for\nsecurity; use the refresh token to obtain a new access token when expired.",
"x-autobe-specification": "JWT access token. Use in Authorization header as \"Bearer {access}\"."
},
refresh: {
type: "string",
description: "Long-lived refresh token for obtaining new access tokens.\n\nUsed to request new access tokens when the current access token expires,\nallowing session continuation without re-authentication. Should be stored\nsecurely and transmitted only to the token refresh endpoint. Typical\nlifetime ranges from 7 to 30 days depending on security requirements.",
"x-autobe-specification": "Refresh token for obtaining new access tokens without re-authentication."
},
expired_at: {
type: "string",
format: "date-time",
description: "ISO 8601 timestamp when the access token expires.\n\nAfter this timestamp, the access token will be rejected by authenticated\nendpoints. Clients should proactively refresh before expiration to maintain\nseamless user experience. A common strategy is to refresh when remaining\ntime falls below 5 minutes. This timestamp is also embedded within the JWT\nitself as the \"exp\" claim.",
"x-autobe-specification": "Access token expiration timestamp in ISO 8601 format."
},
refreshable_until: {
type: "string",
format: "date-time",
description: "ISO 8601 timestamp indicating the absolute session expiration deadline.\n\nRepresents the latest possible time the refresh token can be used. Once\nthis timestamp is reached, the user must fully re-authenticate with\ncredentials. This defines the maximum session duration regardless of\nactivity. If refresh token rotation is enabled, this deadline may extend\nwith each successful refresh.",
"x-autobe-specification": "Refresh token expiration timestamp. Re-authentication required after this time."
}
},
required: [
"access",
"refresh",
"expired_at",
"refreshable_until"
],
description: "JWT-based authorization token pair with expiration metadata.\n\nProvides a complete authentication token structure containing both access and\nrefresh tokens along with their respective expiration timestamps. This\ndual-token pattern enables secure, stateless authentication with automatic\nsession renewal capabilities.\n\nThe access token is short-lived for security, while the refresh token allows\nobtaining new access tokens without requiring the user to re-enter\ncredentials. This structure is automatically included in authentication\nresponses across all generated backend applications.",
"x-autobe-specification": "Dual-token authentication structure with access/refresh tokens and expiration info."
},
IEntity: {
type: "object",
properties: {
id: {
type: "string",
format: "uuid",
description: "Unique identifier for this entity (UUID format).\n\nAuto-generated primary key using UUID format. This value is assigned by the\nsystem upon record creation and cannot be modified afterward. All foreign\nkey relationships in the database reference this field.",
"x-autobe-specification": "Primary key in UUID format. Auto-generated, read-only."
}
},
required: [
"id"
],
description: "Base entity interface providing standard primary key identification.\n\nServes as the foundational interface for all database entities in the\ngenerated application. Every model and record type extends this interface,\nensuring consistent identification semantics across all database tables and\nAPI responses.",
"x-autobe-specification": "Base interface for all database entities. Contains the primary key."
}
}
},
schemas: [
{
$ref: "#/components/schemas/IPage.IPagination"
},
{
$ref: "#/components/schemas/IPage.IRequest"
},
{
$ref: "#/components/schemas/IAuthorizationToken"
},
{
$ref: "#/components/schemas/IEntity"
}
]
}.components) === null || _a === void 0 ? void 0 : _a.schemas) !== null && _b !== void 0 ? _b : {});
for (const value of Object.values(init))
utils_1.AutoBeOpenApiTypeChecker.visit({
components: {
schemas: init,
authorizations: [],
},
schema: value,
closure: (next) => {
if (utils_1.AutoBeOpenApiTypeChecker.isObject(next)) {
next["x-autobe-database-schema"] = null;
}
},
});
return init;
})();
/* -----------------------------------------------------------
PLUGIN
----------------------------------------------------------- */
AutoBeJsonSchemaFactory.fixDesign = (design) => {
const emended = AutoBeJsonSchemaFactory.fixSchema(design.schema);
const final = Object.assign(Object.assign({}, emended), {
description: design.description,
"x-autobe-specification": design.specification,
});
if (utils_1.AutoBeOpenApiTypeChecker.isObject(final))
final["x-autobe-database-schema"] = design.databaseSchema;
return final;
};
AutoBeJsonSchemaFactory.fixSchema = (schema) => {
var _a;
const id = (0, uuid_1.v7)();
const emended = ((_a = utils_2.OpenApiConverter.upgradeComponents({
schemas: {
[id]: schema,
},
}).schemas) !== null && _a !== void 0 ? _a : {})[id];
const visited = new WeakSet();
if (utils_1.AutoBeOpenApiTypeChecker.isObject(emended)) {
visited.add(emended);
for (const v of Object.values(emended.properties))
visited.add(v);
}
utils_1.AutoBeOpenApiTypeChecker.visit({
components: {
authorizations: [],
schemas: {},
},
schema: emended,
closure(next) {
if (visited.has(next) === false)
for (const k of Object.keys(next))
if (k.startsWith("x-")) {
// biome-ignore lint: intended
delete next[k];
}
if (utils_1.AutoBeOpenApiTypeChecker.isString(next))
fixStringSchema(next);
else if (utils_1.AutoBeOpenApiTypeChecker.isArray(next))
fixArraySchema(next);
else if (utils_1.AutoBeOpenApiTypeChecker.isInteger(next))
fixIntegerSchema(next);
else if (utils_1.AutoBeOpenApiTypeChecker.isNumber(next))
fixNumberSchema(next);
},
});
const result = emended;
if (utils_1.AutoBeOpenApiTypeChecker.isObject(result))
for (const [key, value] of Object.entries(result.properties)) {
if (key !== "id" && key.endsWith("_id") === false)
continue;
else if (utils_1.AutoBeOpenApiTypeChecker.isString(value))
fixReferenceIdSchema(value);
else if (utils_1.AutoBeOpenApiTypeChecker.isOneOf(value)) {
const str = value.oneOf.find((v) => utils_1.AutoBeOpenApiTypeChecker.isString(v));
if (str !== undefined)
fixReferenceIdSchema(str);
}
}
return result;
};
const convertConst = (schema, value) => {
// biome-ignore lint: @todo
const description = schema.description;
for (const key of Object.keys(schema)) {
// biome-ignore lint: @todo
delete schema[key];
}
// biome-ignore lint: @todo
schema.const = value;
if (description !== undefined) {
// biome-ignore lint: @todo
schema.description = description;
}
};
const fixStringSchema = (schema) => {
if (schema.format !== undefined) {
delete schema.pattern;
if (schema.format === "uuid" ||
schema.format === "ipv4" ||
schema.format === "ipv6" ||
schema.format === "date" ||
schema.format === "date-time" ||
schema.format === "time") {
delete schema.minLength;
delete schema.maxLength;
delete schema.contentMediaType;
}
}
if (schema.contentMediaType === "")
delete schema.contentMediaType;
if (schema.minLength === 0)
delete schema.minLength;
};
const fixArraySchema = (schema) => {
if (schema.minItems === 0)
delete schema.minItems;
};
/**
* Fix integer schema by converting single valid value ranges to const.
*
* Handles:
*
* - Minimum === maximum → const
* - Minimum: N, exclusiveMaximum: N+1 → const N
* - ExclusiveMinimum: N-1, maximum: N → const N
* - ExclusiveMinimum: N-1, exclusiveMaximum: N+1 → const N
*/
const fixIntegerSchema = (schema) => {
const value = (() => {
if (schema.minimum !== undefined && schema.maximum === schema.minimum)
return schema.minimum;
if (schema.minimum !== undefined &&
schema.exclusiveMaximum === schema.minimum + 1)
return schema.minimum;
if (schema.maximum !== undefined &&
schema.exclusiveMinimum === schema.maximum - 1)
return schema.maximum;
if (schema.exclusiveMinimum !== undefined &&
schema.exclusiveMaximum === schema.exclusiveMinimum + 2)
return schema.exclusiveMinimum + 1;
return undefined;
})();
if (value !== undefined)
convertConst(schema, value);
};
/**
* Fix number schema by converting single valid value ranges to const.
*
* Handles:
*
* - Minimum === maximum → const
*/
const fixNumberSchema = (schema) => {
// minimum === maximum → const
if (schema.minimum !== undefined &&
schema.maximum !== undefined &&
schema.minimum === schema.maximum)
return convertConst(schema, schema.minimum);
};
const fixReferenceIdSchema = (schema) => {
schema.format = "uuid";
fixStringSchema(schema);
};
})(AutoBeJsonSchemaFactory || (exports.AutoBeJsonSchemaFactory = AutoBeJsonSchemaFactory = {}));
//# sourceMappingURL=AutoBeJsonSchemaFactory.js.map