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
|
@@ -157,16 +157,85 @@ function sanitizeSecurity(
|
|
|
157
157
|
});
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Shared, mutable state threaded through the per-route builders. Bundled so
|
|
162
|
+
* `generate()` reads as a flat orchestrator and each helper keeps a single
|
|
163
|
+
* responsibility (parity with the Python/PHP decomposition of `generate`).
|
|
164
|
+
*/
|
|
165
|
+
interface BuildContext {
|
|
166
|
+
models: ModelDefinition[];
|
|
167
|
+
tableToSchema: Map<string, string>;
|
|
168
|
+
schemes: Record<string, Record<string, unknown>>;
|
|
169
|
+
defaultScheme: string;
|
|
170
|
+
includePrefixes: string[];
|
|
171
|
+
excludePrefixes: string[];
|
|
172
|
+
refSchemas: Set<string>;
|
|
173
|
+
usedTags: string[];
|
|
174
|
+
seenIds: Set<string>;
|
|
175
|
+
}
|
|
176
|
+
|
|
160
177
|
export function generate(
|
|
161
178
|
routes: RouteDefinition[],
|
|
162
179
|
models: ModelDefinition[] = []
|
|
163
180
|
): OpenAPISpec {
|
|
181
|
+
const schemes = resolveSecuritySchemes();
|
|
182
|
+
const spec: OpenAPISpec = {
|
|
183
|
+
openapi: resolveOpenApiVersion(),
|
|
184
|
+
info: buildInfo(),
|
|
185
|
+
servers: resolveServers(),
|
|
186
|
+
paths: {},
|
|
187
|
+
components: {
|
|
188
|
+
schemas: {},
|
|
189
|
+
// Configurable security schemes (v3.13.42): bearerFormat via env, optional
|
|
190
|
+
// apiKey scheme, plus any programmatically-registered schemes (which may
|
|
191
|
+
// override bearerAuth — e.g. an oauth2 scheme with scopes).
|
|
192
|
+
securitySchemes: schemes,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// Generate schemas from models, keyed by the model CLASS name ('Item'), which
|
|
197
|
+
// is the type name a generated client wants — never the tableName ('items').
|
|
198
|
+
// `tableToSchema` maps a path-inferred tableName back to that schema key so a
|
|
199
|
+
// POST/PUT request body $ref resolves to the same 'Item' entry.
|
|
200
|
+
const tableToSchema = new Map<string, string>();
|
|
201
|
+
buildComponentSchemas(models, spec, tableToSchema);
|
|
202
|
+
|
|
203
|
+
const ctx: BuildContext = {
|
|
204
|
+
models,
|
|
205
|
+
tableToSchema,
|
|
206
|
+
schemes,
|
|
207
|
+
// Default scheme secured routes use when no explicit meta.security is set.
|
|
208
|
+
defaultScheme: process.env.TINA4_SWAGGER_DEFAULT_SCHEME ?? "bearerAuth",
|
|
209
|
+
// Path filters (comma-separated raw-path prefixes).
|
|
210
|
+
includePrefixes: csv(process.env.TINA4_SWAGGER_INCLUDE),
|
|
211
|
+
excludePrefixes: csv(process.env.TINA4_SWAGGER_EXCLUDE),
|
|
212
|
+
// Reusable custom schemas referenced by routes via meta.requestSchema/responseSchemas.
|
|
213
|
+
refSchemas: new Set<string>(),
|
|
214
|
+
usedTags: [],
|
|
215
|
+
seenIds: new Set<string>(),
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Generate paths from routes
|
|
219
|
+
for (const route of routes) {
|
|
220
|
+
buildOperation(route, spec, ctx);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
buildRefSchemas(spec, ctx.refSchemas);
|
|
224
|
+
buildTags(spec, ctx.usedTags);
|
|
225
|
+
|
|
226
|
+
return spec;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The `info` block: title/version/description, plus optional contact and
|
|
231
|
+
* license. The app's version defaults to 1.0.0 — NOT the framework's (Node
|
|
232
|
+
* shipped 0.0.1) — and description to the empty string, not a canned sentence;
|
|
233
|
+
* both are the settled cross-framework defaults (parity with the Python master),
|
|
234
|
+
* with TINA4_SWAGGER_VERSION / _DESCRIPTION still overriding.
|
|
235
|
+
*/
|
|
236
|
+
function buildInfo(): OpenAPISpecInfo {
|
|
164
237
|
const info: OpenAPISpecInfo = {
|
|
165
238
|
title: process.env.TINA4_SWAGGER_TITLE ?? "Tina4 API",
|
|
166
|
-
// The app's version, defaulting to 1.0.0 — NOT the framework's (Node shipped
|
|
167
|
-
// 0.0.1). description defaults to the empty string, not a canned sentence.
|
|
168
|
-
// Both are the settled cross-framework defaults (parity with the Python
|
|
169
|
-
// master); TINA4_SWAGGER_VERSION / _DESCRIPTION still override.
|
|
170
239
|
version: process.env.TINA4_SWAGGER_VERSION ?? "1.0.0",
|
|
171
240
|
description: process.env.TINA4_SWAGGER_DESCRIPTION ?? "",
|
|
172
241
|
};
|
|
@@ -190,183 +259,220 @@ export function generate(
|
|
|
190
259
|
info.license = url ? { name, url } : { name };
|
|
191
260
|
}
|
|
192
261
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
openapi: resolveOpenApiVersion(),
|
|
196
|
-
info,
|
|
197
|
-
servers: resolveServers(),
|
|
198
|
-
paths: {},
|
|
199
|
-
components: {
|
|
200
|
-
schemas: {},
|
|
201
|
-
// Configurable security schemes (v3.13.42): bearerFormat via env, optional
|
|
202
|
-
// apiKey scheme, plus any programmatically-registered schemes (which may
|
|
203
|
-
// override bearerAuth — e.g. an oauth2 scheme with scopes).
|
|
204
|
-
securitySchemes: schemes,
|
|
205
|
-
},
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
// Default scheme secured routes use when no explicit meta.security is set.
|
|
209
|
-
const defaultScheme = process.env.TINA4_SWAGGER_DEFAULT_SCHEME ?? "bearerAuth";
|
|
210
|
-
|
|
211
|
-
// Path filters (comma-separated raw-path prefixes).
|
|
212
|
-
const includePrefixes = csv(process.env.TINA4_SWAGGER_INCLUDE);
|
|
213
|
-
const excludePrefixes = csv(process.env.TINA4_SWAGGER_EXCLUDE);
|
|
214
|
-
|
|
215
|
-
// Reusable custom schemas referenced by routes via meta.requestSchema/responseSchemas.
|
|
216
|
-
const refSchemas = new Set<string>();
|
|
262
|
+
return info;
|
|
263
|
+
}
|
|
217
264
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
265
|
+
/**
|
|
266
|
+
* Populate `components.schemas` from ORM models (keyed by the class name) and
|
|
267
|
+
* record the tableName -> schema-key map used to resolve request-body $refs.
|
|
268
|
+
*/
|
|
269
|
+
function buildComponentSchemas(
|
|
270
|
+
models: ModelDefinition[],
|
|
271
|
+
spec: OpenAPISpec,
|
|
272
|
+
tableToSchema: Map<string, string>
|
|
273
|
+
): void {
|
|
223
274
|
for (const model of models) {
|
|
224
275
|
const schemaKey = schemaNameForModel(model);
|
|
225
276
|
tableToSchema.set(model.tableName, schemaKey);
|
|
226
277
|
spec.components!.schemas![schemaKey] = modelToSchema(model);
|
|
227
278
|
}
|
|
279
|
+
}
|
|
228
280
|
|
|
229
|
-
|
|
230
|
-
|
|
281
|
+
/**
|
|
282
|
+
* Build ONE path operation for a route and attach it to `spec.paths`, honouring
|
|
283
|
+
* the include/exclude filter and delegating each part of the operation to a
|
|
284
|
+
* focused sub-builder (parameters, request body, response schemas, security).
|
|
285
|
+
*/
|
|
286
|
+
function buildOperation(route: RouteDefinition, spec: OpenAPISpec, ctx: BuildContext): void {
|
|
287
|
+
if (!isIncludedPath(route.pattern, ctx.includePrefixes, ctx.excludePrefixes)) return;
|
|
288
|
+
const openApiPath = patternToOpenAPI(route.pattern);
|
|
289
|
+
const method = route.method.toLowerCase();
|
|
231
290
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const openApiPath = patternToOpenAPI(route.pattern);
|
|
236
|
-
const method = route.method.toLowerCase();
|
|
291
|
+
if (!spec.paths[openApiPath]) {
|
|
292
|
+
spec.paths[openApiPath] = {};
|
|
293
|
+
}
|
|
237
294
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
295
|
+
const tags = route.meta?.tags ?? inferTags(route.pattern);
|
|
296
|
+
for (const t of tags) {
|
|
297
|
+
if (!ctx.usedTags.includes(t)) ctx.usedTags.push(t);
|
|
298
|
+
}
|
|
241
299
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
300
|
+
const operation: Record<string, unknown> = {
|
|
301
|
+
operationId: uniqueOperationId(method, openApiPath, ctx.seenIds),
|
|
302
|
+
summary: route.meta?.summary ?? `${route.method} ${route.pattern}`,
|
|
303
|
+
tags,
|
|
304
|
+
responses: route.meta?.responses ?? {
|
|
305
|
+
"200": { description: "Successful response" },
|
|
306
|
+
},
|
|
307
|
+
};
|
|
246
308
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
summary: route.meta?.summary ?? `${route.method} ${route.pattern}`,
|
|
250
|
-
tags,
|
|
251
|
-
responses: route.meta?.responses ?? {
|
|
252
|
-
"200": { description: "Successful response" },
|
|
253
|
-
},
|
|
254
|
-
};
|
|
309
|
+
if (route.meta?.description) operation.description = route.meta.description;
|
|
310
|
+
if (route.meta?.deprecated) operation.deprecated = true;
|
|
255
311
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
// Add path parameters. Each typed token maps to its JSON Schema fragment
|
|
260
|
-
// (int -> integer, uuid -> string+format, slug -> string+pattern, ...) via
|
|
261
|
-
// PARAM_TYPE_SCHEMA below, mirroring the Python master's _PARAM_TYPE_SCHEMA
|
|
262
|
-
// and the router's accepted token set. An unknown token degrades to string
|
|
263
|
-
// rather than dropping the parameter, and the token never reaches the key.
|
|
264
|
-
const pathParams = extractPathParams(route.pattern);
|
|
265
|
-
if (pathParams.length > 0) {
|
|
266
|
-
operation.parameters = pathParams.map(({ name, schema }) => ({
|
|
267
|
-
name,
|
|
268
|
-
in: "path",
|
|
269
|
-
required: true,
|
|
270
|
-
schema,
|
|
271
|
-
}));
|
|
272
|
-
}
|
|
312
|
+
const parameters = operationParameters(route, method, ctx.models);
|
|
313
|
+
if (parameters.length > 0) operation.parameters = parameters;
|
|
273
314
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
315
|
+
operationRequestBody(route, method, operation, ctx);
|
|
316
|
+
operationResponseSchemas(route, operation, ctx.refSchemas);
|
|
317
|
+
operationSecurity(route, method, operation, ctx.schemes, ctx.defaultScheme);
|
|
318
|
+
|
|
319
|
+
spec.paths[openApiPath][method] = operation;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* The operation's parameters: path parameters (each typed token mapped to its
|
|
324
|
+
* JSON Schema fragment via PARAM_TYPE_SCHEMA — int -> integer, uuid ->
|
|
325
|
+
* string+format, slug -> string+pattern, ...; an unknown token degrades to
|
|
326
|
+
* string rather than dropping the parameter), followed by the page/limit/sort
|
|
327
|
+
* query parameters a GET list endpoint over a known model gets.
|
|
328
|
+
*/
|
|
329
|
+
function operationParameters(
|
|
330
|
+
route: RouteDefinition,
|
|
331
|
+
method: string,
|
|
332
|
+
models: ModelDefinition[]
|
|
333
|
+
): Array<Record<string, unknown>> {
|
|
334
|
+
let parameters: Array<Record<string, unknown>> = [];
|
|
335
|
+
|
|
336
|
+
const pathParams = extractPathParams(route.pattern);
|
|
337
|
+
if (pathParams.length > 0) {
|
|
338
|
+
parameters = pathParams.map(({ name, schema }) => ({
|
|
339
|
+
name,
|
|
340
|
+
in: "path",
|
|
341
|
+
required: true,
|
|
342
|
+
schema,
|
|
343
|
+
}));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Add query parameters for GET list endpoints
|
|
347
|
+
if (method === "get" && !route.pattern.includes("[id]") && !route.pattern.includes("[...")) {
|
|
348
|
+
const modelName = inferModelFromPath(route.pattern);
|
|
349
|
+
if (modelName && models.some((m) => m.tableName === modelName)) {
|
|
350
|
+
parameters = [
|
|
351
|
+
...parameters,
|
|
352
|
+
{ name: "page", in: "query", schema: { type: "integer", default: 1 } },
|
|
353
|
+
{ name: "limit", in: "query", schema: { type: "integer", default: 20 } },
|
|
354
|
+
{ name: "sort", in: "query", schema: { type: "string" }, description: "Sort fields (prefix with - for descending)" },
|
|
355
|
+
];
|
|
285
356
|
}
|
|
357
|
+
}
|
|
286
358
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
359
|
+
return parameters;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** A media-type object: the schema, plus meta.example only when one was given.
|
|
363
|
+
* Key order (schema, then example) matches the emitted document. */
|
|
364
|
+
function mediaWithExample(schema: unknown, example: unknown): Record<string, unknown> {
|
|
365
|
+
const media: Record<string, unknown> = { schema };
|
|
366
|
+
if (example !== undefined) media.example = example;
|
|
367
|
+
return media;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Attach the request body (and, for a path-inferred model write, its 200
|
|
372
|
+
* response schema). A registered custom schema $ref (meta.requestSchema) wins,
|
|
373
|
+
* else the inferred-from-model body (POST/PUT to a resource), else an
|
|
374
|
+
* example-only body.
|
|
375
|
+
*/
|
|
376
|
+
function operationRequestBody(
|
|
377
|
+
route: RouteDefinition,
|
|
378
|
+
method: string,
|
|
379
|
+
operation: Record<string, unknown>,
|
|
380
|
+
ctx: BuildContext
|
|
381
|
+
): void {
|
|
382
|
+
const reqSchemaRef = parseRequestSchema(route.meta?.requestSchema);
|
|
383
|
+
if (reqSchemaRef && (method === "post" || method === "put" || method === "patch")) {
|
|
384
|
+
ctx.refSchemas.add(reqSchemaRef.name);
|
|
385
|
+
const media = mediaWithExample({ $ref: `#/components/schemas/${reqSchemaRef.name}` }, route.meta?.example);
|
|
386
|
+
operation.requestBody = {
|
|
387
|
+
content: { [reqSchemaRef.contentType]: media },
|
|
388
|
+
};
|
|
389
|
+
} else if (method === "post" || method === "put") {
|
|
390
|
+
const modelName = inferModelFromPath(route.pattern);
|
|
391
|
+
const schemaKey = modelName ? ctx.tableToSchema.get(modelName) : undefined;
|
|
392
|
+
if (schemaKey) {
|
|
393
|
+
const sref = `#/components/schemas/${schemaKey}`;
|
|
297
394
|
operation.requestBody = {
|
|
298
|
-
|
|
395
|
+
required: true,
|
|
396
|
+
content: { "application/json": mediaWithExample({ $ref: sref }, route.meta?.example) },
|
|
299
397
|
};
|
|
300
|
-
} else if (method === "post" || method === "put") {
|
|
301
|
-
const modelName = inferModelFromPath(route.pattern);
|
|
302
|
-
const schemaKey = modelName ? tableToSchema.get(modelName) : undefined;
|
|
303
|
-
if (schemaKey) {
|
|
304
|
-
const sref = `#/components/schemas/${schemaKey}`;
|
|
305
|
-
const media: Record<string, unknown> = { schema: { $ref: sref } };
|
|
306
|
-
if (route.meta?.example !== undefined) media.example = route.meta.example;
|
|
307
|
-
operation.requestBody = {
|
|
308
|
-
required: true,
|
|
309
|
-
content: { "application/json": media },
|
|
310
|
-
};
|
|
311
398
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
} else if (route.meta?.example !== undefined) {
|
|
324
|
-
// Non-model body with an explicit example.
|
|
325
|
-
operation.requestBody = {
|
|
326
|
-
content: { "application/json": { schema: inferSchema(route.meta.example), example: route.meta.example } },
|
|
399
|
+
// Response documents 200 with the resource schema — parity with the
|
|
400
|
+
// Python master, which emits ONLY 200 for a model write. The old code
|
|
401
|
+
// stamped an unconditional 422 and a 201 the generator has no way to know
|
|
402
|
+
// a given route returns; both were fiction on a path-inferred write. A
|
|
403
|
+
// route that genuinely answers another code declares it via
|
|
404
|
+
// meta.responses, which is honoured above and never clobbered here.
|
|
405
|
+
if (route.meta?.responses === undefined) {
|
|
406
|
+
operation.responses = {
|
|
407
|
+
"200": { description: "Successful response", content: { "application/json": { schema: { $ref: sref } } } },
|
|
327
408
|
};
|
|
328
409
|
}
|
|
410
|
+
} else if (route.meta?.example !== undefined) {
|
|
411
|
+
// Non-model body with an explicit example.
|
|
412
|
+
operation.requestBody = {
|
|
413
|
+
content: { "application/json": { schema: inferSchema(route.meta.example), example: route.meta.example } },
|
|
414
|
+
};
|
|
329
415
|
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
330
418
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
419
|
+
/**
|
|
420
|
+
* Registered response schemas ($ref) — explicit and authoritative, keyed by
|
|
421
|
+
* status; each maps to the resource, or an array of it when marked as a list.
|
|
422
|
+
*/
|
|
423
|
+
function operationResponseSchemas(
|
|
424
|
+
route: RouteDefinition,
|
|
425
|
+
operation: Record<string, unknown>,
|
|
426
|
+
refSchemas: Set<string>
|
|
427
|
+
): void {
|
|
428
|
+
const respSchemas = parseResponseSchemas(route.meta?.responseSchemas);
|
|
429
|
+
if (respSchemas.length > 0) {
|
|
430
|
+
const responses = operation.responses as Record<string, unknown>;
|
|
431
|
+
for (const { status, name, isList } of respSchemas) {
|
|
432
|
+
refSchemas.add(name);
|
|
433
|
+
const sref = `#/components/schemas/${name}`;
|
|
434
|
+
const schema = isList ? { type: "array", items: { $ref: sref } } : { $ref: sref };
|
|
435
|
+
responses[status] = {
|
|
436
|
+
description: status.startsWith("2") ? "Successful response" : "Response",
|
|
437
|
+
content: { "application/json": { schema } },
|
|
438
|
+
};
|
|
344
439
|
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
345
442
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
443
|
+
/**
|
|
444
|
+
* Per-route security (v3.13.42). Explicit meta.security wins (empty list =
|
|
445
|
+
* explicitly public); otherwise a secured route gets the default scheme (plus
|
|
446
|
+
* ssoSession when bearer + SSO is configured). A secured op also documents a
|
|
447
|
+
* 401. Scopes are kept valid (only oauth2/openIdConnect carry them).
|
|
448
|
+
*/
|
|
449
|
+
function operationSecurity(
|
|
450
|
+
route: RouteDefinition,
|
|
451
|
+
method: string,
|
|
452
|
+
operation: Record<string, unknown>,
|
|
453
|
+
schemes: Record<string, Record<string, unknown>>,
|
|
454
|
+
defaultScheme: string
|
|
455
|
+
): void {
|
|
456
|
+
const hasExplicitSecurity =
|
|
457
|
+
route.meta?.security !== undefined || (route.meta?.scopes !== undefined && route.meta.scopes.length > 0);
|
|
458
|
+
if (hasExplicitSecurity) {
|
|
459
|
+
const normalized = normalizeSecurity(route.meta?.security, route.meta?.scopes);
|
|
460
|
+
operation.security = normalized.length > 0 ? sanitizeSecurity(normalized, schemes) : [];
|
|
461
|
+
if (normalized.length > 0) {
|
|
362
462
|
const responses = operation.responses as Record<string, unknown>;
|
|
363
463
|
if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
|
|
364
464
|
}
|
|
365
|
-
|
|
366
|
-
|
|
465
|
+
} else if (routeRequiresAuth(route, method)) {
|
|
466
|
+
const requirements = [{ [defaultScheme]: [] }];
|
|
467
|
+
if (defaultScheme === "bearerAuth" && schemes.ssoSession) requirements.push({ ssoSession: [] });
|
|
468
|
+
operation.security = sanitizeSecurity(requirements, schemes);
|
|
469
|
+
const responses = operation.responses as Record<string, unknown>;
|
|
470
|
+
if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
|
|
367
471
|
}
|
|
472
|
+
}
|
|
368
473
|
|
|
369
|
-
|
|
474
|
+
/** Registered component schemas referenced via meta.requestSchema/responseSchemas. */
|
|
475
|
+
function buildRefSchemas(spec: OpenAPISpec, refSchemas: Set<string>): void {
|
|
370
476
|
if (refSchemas.size > 0) {
|
|
371
477
|
const schemas = spec.components!.schemas!;
|
|
372
478
|
for (const name of refSchemas) {
|
|
@@ -375,12 +481,13 @@ export function generate(
|
|
|
375
481
|
}
|
|
376
482
|
}
|
|
377
483
|
}
|
|
484
|
+
}
|
|
378
485
|
|
|
486
|
+
/** Top-level tags[] from the tags collected across operations (registration order). */
|
|
487
|
+
function buildTags(spec: OpenAPISpec, usedTags: string[]): void {
|
|
379
488
|
if (usedTags.length > 0) {
|
|
380
489
|
spec.tags = usedTags.map((name) => ({ name }));
|
|
381
490
|
}
|
|
382
|
-
|
|
383
|
-
return spec;
|
|
384
491
|
}
|
|
385
492
|
|
|
386
493
|
function routeRequiresAuth(route: RouteDefinition, method: string): boolean {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { Tina4Request, Tina4Response, RouteHandler, RouteDefinition, RouteMeta, Tina4Config, Middleware, MiddlewareClass, MiddlewareSpec, UploadedFile, CookieOptions, WebSocketRouteHandler, WebSocketRouteDefinition, } from "./types.js";
|
|
2
|
-
export { startServer, resolvePortAndHost, handle, start, stop, httpReason, resolveTemplate, resetTemplateCache, templateAutoRoutingEnabled, isBannerSuppressed } from "./server.js";
|
|
2
|
+
export { startServer, resolvePortAndHost, loopbackBindHosts, handle, start, stop, httpReason, resolveTemplate, resetTemplateCache, templateAutoRoutingEnabled, isBannerSuppressed } from "./server.js";
|
|
3
3
|
export { background, stopAllBackgroundTasks, backgroundTaskCount } from "./background.js";
|
|
4
4
|
export { Router, RouteGroup, RouteRef, WsRouteRef, defaultRouter, runRouteMiddlewares, resolveStringMiddleware, isTrailingSlashRedirectEnabled } from "./router.js";
|
|
5
5
|
export { get, post, put, patch, del, any, websocket, del as delete } from "./router.js";
|
|
@@ -249,6 +249,21 @@ export declare function buildDispatchContext(router: Router, base?: string): Pro
|
|
|
249
249
|
* skipped for one caller and not the other (feature 131, TC-DEC-01).
|
|
250
250
|
*/
|
|
251
251
|
export declare function runDispatch(ctx: DispatchContext, rawReq: IncomingMessage, rawRes: ServerResponse): Promise<void>;
|
|
252
|
+
/**
|
|
253
|
+
* Sibling loopback addresses to ALSO listen on, so `localhost` reaches this
|
|
254
|
+
* server whether the OS resolves it to IPv4 (127.0.0.1) or IPv6 (::1).
|
|
255
|
+
*
|
|
256
|
+
* `localhost` resolves to `::1` (IPv6) FIRST on Windows, so a server bound only
|
|
257
|
+
* to `127.0.0.1` — or `0.0.0.0`, the IPv4 wildcard, which does NOT cover IPv6 —
|
|
258
|
+
* refuses the browser with ERR_CONNECTION_REFUSED even though it is serving,
|
|
259
|
+
* because nothing listens on `::1`. Binding the sibling family closes that gap.
|
|
260
|
+
*
|
|
261
|
+
* Returns only the families a direct bind of `host` does not already cover, as
|
|
262
|
+
* UNBRACKETED addresses (Node's net/http take a bare "::1"). A host that is
|
|
263
|
+
* neither loopback nor a wildcard yields an empty list — an explicit LAN
|
|
264
|
+
* address is bound exactly as asked. Mirrors PHP `Server::loopbackBindHosts`.
|
|
265
|
+
*/
|
|
266
|
+
export declare function loopbackBindHosts(host: string): string[];
|
|
252
267
|
export declare function startServer(config?: Tina4Config): Promise<{
|
|
253
268
|
close: () => void;
|
|
254
269
|
router: Router;
|
|
@@ -257,6 +257,17 @@ export declare class Frond {
|
|
|
257
257
|
*/
|
|
258
258
|
private handleImportAs;
|
|
259
259
|
private handleFromImport;
|
|
260
|
+
/**
|
|
261
|
+
* Collect the body tokens of a {% <openTag> %}...{% end<openTag> %} block,
|
|
262
|
+
* starting from the token after the opening tag (start + 1). Nested same-tag
|
|
263
|
+
* blocks are kept in the body and balanced by depth; the matching closing tag is
|
|
264
|
+
* consumed but NOT included. Returns [bodyTokens, indexAfterClosingTag].
|
|
265
|
+
*
|
|
266
|
+
* canNest guards the open-tag count: handleSetBlock passes it so the inline
|
|
267
|
+
* {% set x = 1 %} form (which has no {% endset %}) never opens a nested block —
|
|
268
|
+
* only the block form {% set x %} nests. Omitted, every openTag occurrence nests.
|
|
269
|
+
*/
|
|
270
|
+
private collectBlockBody;
|
|
260
271
|
private handleCache;
|
|
261
272
|
/**
|
|
262
273
|
* Handle {% live "name" poll N | sse | ws "path" [src "url"] %}...{% endlive %}.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ModelCollection } from "./modelCollection.js";
|
|
1
2
|
import { QueryBuilder } from "./queryBuilder.js";
|
|
2
3
|
import type { DatabaseAdapter, FieldDefinition, RelationshipDefinition } from "./types.js";
|
|
3
4
|
/**
|
|
@@ -135,6 +136,20 @@ export declare class BaseModel {
|
|
|
135
136
|
* Get the primary key database column name (applies fieldMapping).
|
|
136
137
|
*/
|
|
137
138
|
protected static getPkColumn(): string;
|
|
139
|
+
/**
|
|
140
|
+
* Shared read tail for the collection-returning finders (where / all / select
|
|
141
|
+
* / find filter-form / withTrashed). Runs the SAME two calls `db.fetch()`
|
|
142
|
+
* makes — the page fetch AND the COUNT probe over the SAME base SQL — hydrates
|
|
143
|
+
* the rows into model instances, and returns a ModelCollection carrying the
|
|
144
|
+
* total (ADR-0064).
|
|
145
|
+
*
|
|
146
|
+
* The total is FREE: `probeTotal` is the exact `COUNT(*)` probe `db.fetch()`
|
|
147
|
+
* already runs; the ORM used to discard it. ZERO extra queries beyond that one
|
|
148
|
+
* probe. `sql` MUST NOT carry its own LIMIT/OFFSET — `adapterFetch` applies
|
|
149
|
+
* limit/offset to the page, and the probe wraps the un-limited SQL so it counts
|
|
150
|
+
* the WHOLE filtered set, not the page.
|
|
151
|
+
*/
|
|
152
|
+
protected static _collect<T extends BaseModel>(this: typeof BaseModel & (new (data?: Record<string, unknown>) => T), sql: string, params: unknown[] | undefined, limit: number, offset: number, include?: string[]): Promise<ModelCollection<T>>;
|
|
138
153
|
/**
|
|
139
154
|
* Find a record by primary key.
|
|
140
155
|
* @param id Primary key value.
|
|
@@ -174,7 +189,7 @@ export declare class BaseModel {
|
|
|
174
189
|
* User.find() → all records
|
|
175
190
|
*/
|
|
176
191
|
static find<T extends BaseModel>(this: new (data?: Record<string, unknown>) => T, pk: number | string, include?: string[]): Promise<T | null>;
|
|
177
|
-
static find<T extends BaseModel>(this: new (data?: Record<string, unknown>) => T, filter?: Record<string, unknown>, limit?: number, offset?: number, orderBy?: string, include?: string[]): Promise<T
|
|
192
|
+
static find<T extends BaseModel>(this: new (data?: Record<string, unknown>) => T, filter?: Record<string, unknown>, limit?: number, offset?: number, orderBy?: string, include?: string[]): Promise<ModelCollection<T>>;
|
|
178
193
|
/**
|
|
179
194
|
* Load a record into this instance via selectOne.
|
|
180
195
|
* Returns true if found and loaded, false otherwise.
|
|
@@ -218,7 +233,7 @@ export declare class BaseModel {
|
|
|
218
233
|
* @param include Relationship names to eager-load.
|
|
219
234
|
* @param orderBy ORDER BY clause (e.g. "name ASC").
|
|
220
235
|
*/
|
|
221
|
-
static all<T extends BaseModel>(this: new (data?: Record<string, unknown>) => T, limit?: number, offset?: number, include?: string[], orderBy?: string): Promise<T
|
|
236
|
+
static all<T extends BaseModel>(this: new (data?: Record<string, unknown>) => T, limit?: number, offset?: number, include?: string[], orderBy?: string): Promise<ModelCollection<T>>;
|
|
222
237
|
/**
|
|
223
238
|
* Query records with a WHERE clause.
|
|
224
239
|
* Matches Python/PHP/Ruby where() API.
|
|
@@ -230,7 +245,7 @@ export declare class BaseModel {
|
|
|
230
245
|
* @param include Relationship names to eager-load
|
|
231
246
|
* @param orderBy ORDER BY clause (e.g. "name ASC")
|
|
232
247
|
*/
|
|
233
|
-
static where<T extends BaseModel>(this: new (data?: Record<string, unknown>) => T, conditions: string, params?: unknown[], limit?: number, offset?: number, include?: string[], orderBy?: string): Promise<T
|
|
248
|
+
static where<T extends BaseModel>(this: new (data?: Record<string, unknown>) => T, conditions: string, params?: unknown[], limit?: number, offset?: number, include?: string[], orderBy?: string): Promise<ModelCollection<T>>;
|
|
234
249
|
/**
|
|
235
250
|
* Save this instance (insert or update). Returns this on success (fluent
|
|
236
251
|
* self), false on failure.
|
|
@@ -354,7 +369,7 @@ export declare class BaseModel {
|
|
|
354
369
|
/**
|
|
355
370
|
* Execute a raw SQL SELECT and return results as model instances.
|
|
356
371
|
*/
|
|
357
|
-
static select<T extends BaseModel>(this: new (data?: Record<string, unknown>) => T, sql: string, params?: unknown[], limit?: number, offset?: number): Promise<T
|
|
372
|
+
static select<T extends BaseModel>(this: new (data?: Record<string, unknown>) => T, sql: string, params?: unknown[], limit?: number, offset?: number): Promise<ModelCollection<T>>;
|
|
358
373
|
static selectOne<T extends BaseModel>(this: new (data?: Record<string, unknown>) => T, sql: string, params?: unknown[], include?: string[]): Promise<T | null>;
|
|
359
374
|
/**
|
|
360
375
|
* Permanently delete this instance, bypassing soft delete.
|
|
@@ -367,7 +382,7 @@ export declare class BaseModel {
|
|
|
367
382
|
/**
|
|
368
383
|
* Find records including soft-deleted ones.
|
|
369
384
|
*/
|
|
370
|
-
static withTrashed<T extends BaseModel>(this: new (data?: Record<string, unknown>) => T, conditions?: string, params?: unknown[], limit?: number, offset?: number): Promise<T
|
|
385
|
+
static withTrashed<T extends BaseModel>(this: new (data?: Record<string, unknown>) => T, conditions?: string, params?: unknown[], limit?: number, offset?: number): Promise<ModelCollection<T>>;
|
|
371
386
|
/**
|
|
372
387
|
* Count records matching conditions (respects soft delete and table filter).
|
|
373
388
|
*/
|
package/types/orm/src/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export type { FieldType, FieldDefinition, ModelDefinition, DatabaseAdapter, Data
|
|
|
2
2
|
export { REQUIRED_ADAPTER_CAPABILITIES, NOT_REQUIRED_ON_ADAPTER } from "./types.js";
|
|
3
3
|
export { DatabaseResult } from "./databaseResult.js";
|
|
4
4
|
export type { ColumnInfoResult } from "./databaseResult.js";
|
|
5
|
+
export { ModelCollection } from "./modelCollection.js";
|
|
6
|
+
export type { PaginateEnvelope } from "./modelCollection.js";
|
|
5
7
|
export { Database, initDatabase, getAdapter, setAdapter, bindDatabase, createAdapterFromUrl, closeDatabase, parseDatabaseUrl, setNamedAdapter, getNamedAdapter, resolveDbPool, stripTrailingSemicolons, wrapWithCache, resetRequestCaches } from "./database.js";
|
|
6
8
|
export { adapterFetch, adapterQuery, adapterFetchOne, adapterExecute, adapterInsert, adapterStartTransaction, adapterCommit, adapterRollback, adapterTableExists, adapterTables, adapterColumns, adapterCreateTable, extractLastInsertId, } from "./database.js";
|
|
7
9
|
export type { DatabaseConfig } from "./database.js";
|