tina4-nodejs 3.13.131 → 3.13.133
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +3 -2
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +398 -302
- package/packages/core/dist/index.js +399 -302
- package/packages/core/public/js/tina4-dev-admin.min.js +148 -512
- package/packages/core/src/devAdmin.ts +0 -46
- package/packages/core/src/index.ts +1 -1
- package/packages/core/src/server.ts +129 -3
- package/packages/frond/dist/index.js +52 -104
- package/packages/frond/src/engine.ts +52 -100
- package/packages/orm/dist/index.js +399 -302
- package/packages/orm/src/baseModel.ts +64 -55
- package/packages/orm/src/index.ts +2 -0
- package/packages/orm/src/modelCollection.ts +149 -0
- package/packages/swagger/dist/index.js +150 -116
- package/packages/swagger/src/generator.ts +261 -154
- package/types/core/src/index.d.ts +1 -1
- package/types/core/src/server.d.ts +15 -0
- package/types/frond/src/engine.d.ts +11 -0
- package/types/orm/src/baseModel.d.ts +20 -5
- package/types/orm/src/index.d.ts +2 -0
- package/types/orm/src/modelCollection.d.ts +101 -0
|
@@ -80,12 +80,46 @@ function sanitizeSecurity(reqs, schemes) {
|
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
82
|
function generate(routes, models = []) {
|
|
83
|
+
const schemes = resolveSecuritySchemes();
|
|
84
|
+
const spec = {
|
|
85
|
+
openapi: resolveOpenApiVersion(),
|
|
86
|
+
info: buildInfo(),
|
|
87
|
+
servers: resolveServers(),
|
|
88
|
+
paths: {},
|
|
89
|
+
components: {
|
|
90
|
+
schemas: {},
|
|
91
|
+
// Configurable security schemes (v3.13.42): bearerFormat via env, optional
|
|
92
|
+
// apiKey scheme, plus any programmatically-registered schemes (which may
|
|
93
|
+
// override bearerAuth — e.g. an oauth2 scheme with scopes).
|
|
94
|
+
securitySchemes: schemes
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const tableToSchema = /* @__PURE__ */ new Map();
|
|
98
|
+
buildComponentSchemas(models, spec, tableToSchema);
|
|
99
|
+
const ctx = {
|
|
100
|
+
models,
|
|
101
|
+
tableToSchema,
|
|
102
|
+
schemes,
|
|
103
|
+
// Default scheme secured routes use when no explicit meta.security is set.
|
|
104
|
+
defaultScheme: process.env.TINA4_SWAGGER_DEFAULT_SCHEME ?? "bearerAuth",
|
|
105
|
+
// Path filters (comma-separated raw-path prefixes).
|
|
106
|
+
includePrefixes: csv(process.env.TINA4_SWAGGER_INCLUDE),
|
|
107
|
+
excludePrefixes: csv(process.env.TINA4_SWAGGER_EXCLUDE),
|
|
108
|
+
// Reusable custom schemas referenced by routes via meta.requestSchema/responseSchemas.
|
|
109
|
+
refSchemas: /* @__PURE__ */ new Set(),
|
|
110
|
+
usedTags: [],
|
|
111
|
+
seenIds: /* @__PURE__ */ new Set()
|
|
112
|
+
};
|
|
113
|
+
for (const route of routes) {
|
|
114
|
+
buildOperation(route, spec, ctx);
|
|
115
|
+
}
|
|
116
|
+
buildRefSchemas(spec, ctx.refSchemas);
|
|
117
|
+
buildTags(spec, ctx.usedTags);
|
|
118
|
+
return spec;
|
|
119
|
+
}
|
|
120
|
+
function buildInfo() {
|
|
83
121
|
const info = {
|
|
84
122
|
title: process.env.TINA4_SWAGGER_TITLE ?? "Tina4 API",
|
|
85
|
-
// The app's version, defaulting to 1.0.0 — NOT the framework's (Node shipped
|
|
86
|
-
// 0.0.1). description defaults to the empty string, not a canned sentence.
|
|
87
|
-
// Both are the settled cross-framework defaults (parity with the Python
|
|
88
|
-
// master); TINA4_SWAGGER_VERSION / _DESCRIPTION still override.
|
|
89
123
|
version: process.env.TINA4_SWAGGER_VERSION ?? "1.0.0",
|
|
90
124
|
description: process.env.TINA4_SWAGGER_DESCRIPTION ?? ""
|
|
91
125
|
};
|
|
@@ -102,135 +136,134 @@ function generate(routes, models = []) {
|
|
|
102
136
|
const [name, url] = licenseRaw.split("|").map((s) => s.trim());
|
|
103
137
|
info.license = url ? { name, url } : { name };
|
|
104
138
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
info,
|
|
109
|
-
servers: resolveServers(),
|
|
110
|
-
paths: {},
|
|
111
|
-
components: {
|
|
112
|
-
schemas: {},
|
|
113
|
-
// Configurable security schemes (v3.13.42): bearerFormat via env, optional
|
|
114
|
-
// apiKey scheme, plus any programmatically-registered schemes (which may
|
|
115
|
-
// override bearerAuth — e.g. an oauth2 scheme with scopes).
|
|
116
|
-
securitySchemes: schemes
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
const defaultScheme = process.env.TINA4_SWAGGER_DEFAULT_SCHEME ?? "bearerAuth";
|
|
120
|
-
const includePrefixes = csv(process.env.TINA4_SWAGGER_INCLUDE);
|
|
121
|
-
const excludePrefixes = csv(process.env.TINA4_SWAGGER_EXCLUDE);
|
|
122
|
-
const refSchemas = /* @__PURE__ */ new Set();
|
|
123
|
-
const tableToSchema = /* @__PURE__ */ new Map();
|
|
139
|
+
return info;
|
|
140
|
+
}
|
|
141
|
+
function buildComponentSchemas(models, spec, tableToSchema) {
|
|
124
142
|
for (const model of models) {
|
|
125
143
|
const schemaKey = schemaNameForModel(model);
|
|
126
144
|
tableToSchema.set(model.tableName, schemaKey);
|
|
127
145
|
spec.components.schemas[schemaKey] = modelToSchema(model);
|
|
128
146
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
147
|
+
}
|
|
148
|
+
function buildOperation(route, spec, ctx) {
|
|
149
|
+
if (!isIncludedPath(route.pattern, ctx.includePrefixes, ctx.excludePrefixes)) return;
|
|
150
|
+
const openApiPath = patternToOpenAPI(route.pattern);
|
|
151
|
+
const method = route.method.toLowerCase();
|
|
152
|
+
if (!spec.paths[openApiPath]) {
|
|
153
|
+
spec.paths[openApiPath] = {};
|
|
154
|
+
}
|
|
155
|
+
const tags = route.meta?.tags ?? inferTags(route.pattern);
|
|
156
|
+
for (const t of tags) {
|
|
157
|
+
if (!ctx.usedTags.includes(t)) ctx.usedTags.push(t);
|
|
158
|
+
}
|
|
159
|
+
const operation = {
|
|
160
|
+
operationId: uniqueOperationId(method, openApiPath, ctx.seenIds),
|
|
161
|
+
summary: route.meta?.summary ?? `${route.method} ${route.pattern}`,
|
|
162
|
+
tags,
|
|
163
|
+
responses: route.meta?.responses ?? {
|
|
164
|
+
"200": { description: "Successful response" }
|
|
137
165
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
166
|
+
};
|
|
167
|
+
if (route.meta?.description) operation.description = route.meta.description;
|
|
168
|
+
if (route.meta?.deprecated) operation.deprecated = true;
|
|
169
|
+
const parameters = operationParameters(route, method, ctx.models);
|
|
170
|
+
if (parameters.length > 0) operation.parameters = parameters;
|
|
171
|
+
operationRequestBody(route, method, operation, ctx);
|
|
172
|
+
operationResponseSchemas(route, operation, ctx.refSchemas);
|
|
173
|
+
operationSecurity(route, method, operation, ctx.schemes, ctx.defaultScheme);
|
|
174
|
+
spec.paths[openApiPath][method] = operation;
|
|
175
|
+
}
|
|
176
|
+
function operationParameters(route, method, models) {
|
|
177
|
+
let parameters = [];
|
|
178
|
+
const pathParams = extractPathParams(route.pattern);
|
|
179
|
+
if (pathParams.length > 0) {
|
|
180
|
+
parameters = pathParams.map(({ name, schema }) => ({
|
|
181
|
+
name,
|
|
182
|
+
in: "path",
|
|
183
|
+
required: true,
|
|
184
|
+
schema
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
if (method === "get" && !route.pattern.includes("[id]") && !route.pattern.includes("[...")) {
|
|
188
|
+
const modelName = inferModelFromPath(route.pattern);
|
|
189
|
+
if (modelName && models.some((m) => m.tableName === modelName)) {
|
|
190
|
+
parameters = [
|
|
191
|
+
...parameters,
|
|
192
|
+
{ name: "page", in: "query", schema: { type: "integer", default: 1 } },
|
|
193
|
+
{ name: "limit", in: "query", schema: { type: "integer", default: 20 } },
|
|
194
|
+
{ name: "sort", in: "query", schema: { type: "string" }, description: "Sort fields (prefix with - for descending)" }
|
|
195
|
+
];
|
|
141
196
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
197
|
+
}
|
|
198
|
+
return parameters;
|
|
199
|
+
}
|
|
200
|
+
function mediaWithExample(schema, example) {
|
|
201
|
+
const media = { schema };
|
|
202
|
+
if (example !== void 0) media.example = example;
|
|
203
|
+
return media;
|
|
204
|
+
}
|
|
205
|
+
function operationRequestBody(route, method, operation, ctx) {
|
|
206
|
+
const reqSchemaRef = parseRequestSchema(route.meta?.requestSchema);
|
|
207
|
+
if (reqSchemaRef && (method === "post" || method === "put" || method === "patch")) {
|
|
208
|
+
ctx.refSchemas.add(reqSchemaRef.name);
|
|
209
|
+
const media = mediaWithExample({ $ref: `#/components/schemas/${reqSchemaRef.name}` }, route.meta?.example);
|
|
210
|
+
operation.requestBody = {
|
|
211
|
+
content: { [reqSchemaRef.contentType]: media }
|
|
149
212
|
};
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
name,
|
|
156
|
-
in: "path",
|
|
157
|
-
required: true,
|
|
158
|
-
schema
|
|
159
|
-
}));
|
|
160
|
-
}
|
|
161
|
-
if (method === "get" && !route.pattern.includes("[id]") && !route.pattern.includes("[...")) {
|
|
162
|
-
const modelName = inferModelFromPath(route.pattern);
|
|
163
|
-
if (modelName && models.some((m) => m.tableName === modelName)) {
|
|
164
|
-
operation.parameters = [
|
|
165
|
-
...operation.parameters ?? [],
|
|
166
|
-
{ name: "page", in: "query", schema: { type: "integer", default: 1 } },
|
|
167
|
-
{ name: "limit", in: "query", schema: { type: "integer", default: 20 } },
|
|
168
|
-
{ name: "sort", in: "query", schema: { type: "string" }, description: "Sort fields (prefix with - for descending)" }
|
|
169
|
-
];
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
const reqSchemaRef = parseRequestSchema(route.meta?.requestSchema);
|
|
173
|
-
if (reqSchemaRef && (method === "post" || method === "put" || method === "patch")) {
|
|
174
|
-
refSchemas.add(reqSchemaRef.name);
|
|
175
|
-
const media = {
|
|
176
|
-
schema: { $ref: `#/components/schemas/${reqSchemaRef.name}` }
|
|
177
|
-
};
|
|
178
|
-
if (route.meta?.example !== void 0) media.example = route.meta.example;
|
|
213
|
+
} else if (method === "post" || method === "put") {
|
|
214
|
+
const modelName = inferModelFromPath(route.pattern);
|
|
215
|
+
const schemaKey = modelName ? ctx.tableToSchema.get(modelName) : void 0;
|
|
216
|
+
if (schemaKey) {
|
|
217
|
+
const sref = `#/components/schemas/${schemaKey}`;
|
|
179
218
|
operation.requestBody = {
|
|
180
|
-
|
|
219
|
+
required: true,
|
|
220
|
+
content: { "application/json": mediaWithExample({ $ref: sref }, route.meta?.example) }
|
|
181
221
|
};
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (schemaKey) {
|
|
186
|
-
const sref = `#/components/schemas/${schemaKey}`;
|
|
187
|
-
const media = { schema: { $ref: sref } };
|
|
188
|
-
if (route.meta?.example !== void 0) media.example = route.meta.example;
|
|
189
|
-
operation.requestBody = {
|
|
190
|
-
required: true,
|
|
191
|
-
content: { "application/json": media }
|
|
192
|
-
};
|
|
193
|
-
if (route.meta?.responses === void 0) {
|
|
194
|
-
operation.responses = {
|
|
195
|
-
"200": { description: "Successful response", content: { "application/json": { schema: { $ref: sref } } } }
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
} else if (route.meta?.example !== void 0) {
|
|
199
|
-
operation.requestBody = {
|
|
200
|
-
content: { "application/json": { schema: inferSchema(route.meta.example), example: route.meta.example } }
|
|
222
|
+
if (route.meta?.responses === void 0) {
|
|
223
|
+
operation.responses = {
|
|
224
|
+
"200": { description: "Successful response", content: { "application/json": { schema: { $ref: sref } } } }
|
|
201
225
|
};
|
|
202
226
|
}
|
|
227
|
+
} else if (route.meta?.example !== void 0) {
|
|
228
|
+
operation.requestBody = {
|
|
229
|
+
content: { "application/json": { schema: inferSchema(route.meta.example), example: route.meta.example } }
|
|
230
|
+
};
|
|
203
231
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function operationResponseSchemas(route, operation, refSchemas) {
|
|
235
|
+
const respSchemas = parseResponseSchemas(route.meta?.responseSchemas);
|
|
236
|
+
if (respSchemas.length > 0) {
|
|
237
|
+
const responses = operation.responses;
|
|
238
|
+
for (const { status, name, isList } of respSchemas) {
|
|
239
|
+
refSchemas.add(name);
|
|
240
|
+
const sref = `#/components/schemas/${name}`;
|
|
241
|
+
const schema = isList ? { type: "array", items: { $ref: sref } } : { $ref: sref };
|
|
242
|
+
responses[status] = {
|
|
243
|
+
description: status.startsWith("2") ? "Successful response" : "Response",
|
|
244
|
+
content: { "application/json": { schema } }
|
|
245
|
+
};
|
|
216
246
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
} else if (routeRequiresAuth(route, method)) {
|
|
226
|
-
const requirements = [{ [defaultScheme]: [] }];
|
|
227
|
-
if (defaultScheme === "bearerAuth" && schemes.ssoSession) requirements.push({ ssoSession: [] });
|
|
228
|
-
operation.security = sanitizeSecurity(requirements, schemes);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function operationSecurity(route, method, operation, schemes, defaultScheme) {
|
|
250
|
+
const hasExplicitSecurity = route.meta?.security !== void 0 || route.meta?.scopes !== void 0 && route.meta.scopes.length > 0;
|
|
251
|
+
if (hasExplicitSecurity) {
|
|
252
|
+
const normalized = normalizeSecurity(route.meta?.security, route.meta?.scopes);
|
|
253
|
+
operation.security = normalized.length > 0 ? sanitizeSecurity(normalized, schemes) : [];
|
|
254
|
+
if (normalized.length > 0) {
|
|
229
255
|
const responses = operation.responses;
|
|
230
256
|
if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
|
|
231
257
|
}
|
|
232
|
-
|
|
258
|
+
} else if (routeRequiresAuth(route, method)) {
|
|
259
|
+
const requirements = [{ [defaultScheme]: [] }];
|
|
260
|
+
if (defaultScheme === "bearerAuth" && schemes.ssoSession) requirements.push({ ssoSession: [] });
|
|
261
|
+
operation.security = sanitizeSecurity(requirements, schemes);
|
|
262
|
+
const responses = operation.responses;
|
|
263
|
+
if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
|
|
233
264
|
}
|
|
265
|
+
}
|
|
266
|
+
function buildRefSchemas(spec, refSchemas) {
|
|
234
267
|
if (refSchemas.size > 0) {
|
|
235
268
|
const schemas = spec.components.schemas;
|
|
236
269
|
for (const name of refSchemas) {
|
|
@@ -239,10 +272,11 @@ function generate(routes, models = []) {
|
|
|
239
272
|
}
|
|
240
273
|
}
|
|
241
274
|
}
|
|
275
|
+
}
|
|
276
|
+
function buildTags(spec, usedTags) {
|
|
242
277
|
if (usedTags.length > 0) {
|
|
243
278
|
spec.tags = usedTags.map((name) => ({ name }));
|
|
244
279
|
}
|
|
245
|
-
return spec;
|
|
246
280
|
}
|
|
247
281
|
function routeRequiresAuth(route, method) {
|
|
248
282
|
if (route.noAuth) return false;
|