tina4-nodejs 3.13.132 → 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 +151 -117
- package/packages/core/dist/index.js +151 -117
- package/packages/orm/dist/index.js +151 -117
- package/packages/swagger/dist/index.js +150 -116
- package/packages/swagger/src/generator.ts +261 -154
package/CLAUDE.md
CHANGED
|
@@ -13,13 +13,13 @@ Even if the skill text is not currently loaded, these are non-negotiable:
|
|
|
13
13
|
|
|
14
14
|
The full discipline lives in `.claude/skills/tina4-maintainer/SKILL.md`; this block is the always-on floor.
|
|
15
15
|
|
|
16
|
-
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.
|
|
16
|
+
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.133)
|
|
17
17
|
|
|
18
18
|
> This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
|
|
19
19
|
|
|
20
20
|
## What This Project Is
|
|
21
21
|
|
|
22
|
-
Tina4 for Node.js/TypeScript v3.13.
|
|
22
|
+
Tina4 for Node.js/TypeScript v3.13.133 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
|
|
23
23
|
|
|
24
24
|
The philosophy: zero ceremony, batteries included, file system as source of truth.
|
|
25
25
|
|
|
@@ -172,6 +172,7 @@ Database layer with auto-CRUD generation, seeding, fake data, and SQL translatio
|
|
|
172
172
|
- `sqlTranslator.ts` — Cross-engine SQL translator (`SQLTranslator`) and TTL query cache (`QueryCache`)
|
|
173
173
|
- **Instance methods:** `save(): this|false` (fluent, false on failure), `delete()`, `forceDelete()`, `restore()`, `load(sql, params?, include?): boolean`, `validate(): string[]`, `toDict(include?)`, `toAssoc(include?)`, `toObject()`, `toArray(): unknown[]`, `toList()`, `toJson(include?)`, `hasOne(class, fk)`, `hasMany(class, fk, limit?, offset?)`, `belongsTo(class, fk)`
|
|
174
174
|
- **Static methods:** `find(id, include?)`, `findById(id, include?)`, `findOrFail(id)`, `create(data)`, `all(limit=100, offset=0, include?, orderBy?)`, `select(sql, params?, limit=100, offset=0)`, `selectOne(sql, params?, include?)`, `where(conditions, params?, limit=100, offset=0, include?, orderBy?)`, `count(conditions?, params?)`, `withTrashed(conditions?, params?, limit=100, offset=0)`, `scope(name, filterSql, params?)` (registers reusable method), `createTable()`, `query()`, `_processForeignKeys()`, `_applyFkRegistry()`
|
|
175
|
+
- **ModelCollection (ADR-0064):** `where`, `select`, `find` (filter form), `all`, and `withTrashed` return a `ModelCollection<T>` (extends `Array<T>`, so `Array.isArray` stays true and iterate/index/`.length`/`JSON.stringify` are unchanged) that ALSO carries the filter total, independent of limit/offset: `rows.getTotalRecords()` (e.g. 250, reuses the query COUNT -- zero extra queries) and `rows.toPaginate()` (`{ records, total, page, per_page, total_pages, limit, offset }`). A method, not a `.count` property. Single-record finders (`findById`, `selectOne`) are unchanged.
|
|
175
176
|
- **Foreign key auto-wire (declarative, read-side-only):** Declare a field with `type: "foreignKey"` and `references: "ModelName"` to auto-wire both `belongsTo` on the declaring model and `hasMany` on the referenced model. Optional `relatedName` overrides the has-many key. Models must be registered via `BaseModel.registerModel(name, class)` for name-based resolution. Example: `author_id: { type: "foreignKey", references: "Author", relatedName: "posts" }` attaches LAZY accessors on both sides — `await post.author` (belongsTo) and `await author.posts` (hasMany) resolve on attribute access (async, cached), and `include: ["posts"]` on `find`/`all`/`where` eager-loads them in ONE query per relation. A soft-deleted child is excluded from traversal, and the has-many read is uncapped. The auto-wire emits NO DB-level FK / ON DELETE clause (REL-DEC-01, read-side-only): referential integrity is the migration/DDL's job, so deleting a parent does not cascade to children at the engine level. (Before 3.13.99 the declarative accessors did not attach and lazy load did not exist — only the imperative `post.belongsTo(Author, "author_id")` / `author.hasMany(Post, "author_id")` worked.)
|
|
176
177
|
- QueryBuilder supports `toMongo()` for generating MongoDB query documents from the same fluent API
|
|
177
178
|
- `getNextId(table: string, pkColumn?: string, generatorName?: string): Promise<number>` — Race-safe ID generation using atomic sequence table (`tina4_sequences`). SQLite/MySQL/MSSQL use `tina4_sequences` with atomic UPDATE+SELECT. PostgreSQL auto-creates sequences if missing. Firebird uses existing generators (unchanged).
|
package/package.json
CHANGED
package/packages/cli/dist/bin.js
CHANGED
|
@@ -37635,12 +37635,46 @@ function sanitizeSecurity(reqs, schemes) {
|
|
|
37635
37635
|
});
|
|
37636
37636
|
}
|
|
37637
37637
|
function generate2(routes, models = []) {
|
|
37638
|
+
const schemes = resolveSecuritySchemes();
|
|
37639
|
+
const spec = {
|
|
37640
|
+
openapi: resolveOpenApiVersion(),
|
|
37641
|
+
info: buildInfo(),
|
|
37642
|
+
servers: resolveServers(),
|
|
37643
|
+
paths: {},
|
|
37644
|
+
components: {
|
|
37645
|
+
schemas: {},
|
|
37646
|
+
// Configurable security schemes (v3.13.42): bearerFormat via env, optional
|
|
37647
|
+
// apiKey scheme, plus any programmatically-registered schemes (which may
|
|
37648
|
+
// override bearerAuth — e.g. an oauth2 scheme with scopes).
|
|
37649
|
+
securitySchemes: schemes
|
|
37650
|
+
}
|
|
37651
|
+
};
|
|
37652
|
+
const tableToSchema = /* @__PURE__ */ new Map();
|
|
37653
|
+
buildComponentSchemas(models, spec, tableToSchema);
|
|
37654
|
+
const ctx = {
|
|
37655
|
+
models,
|
|
37656
|
+
tableToSchema,
|
|
37657
|
+
schemes,
|
|
37658
|
+
// Default scheme secured routes use when no explicit meta.security is set.
|
|
37659
|
+
defaultScheme: process.env.TINA4_SWAGGER_DEFAULT_SCHEME ?? "bearerAuth",
|
|
37660
|
+
// Path filters (comma-separated raw-path prefixes).
|
|
37661
|
+
includePrefixes: csv(process.env.TINA4_SWAGGER_INCLUDE),
|
|
37662
|
+
excludePrefixes: csv(process.env.TINA4_SWAGGER_EXCLUDE),
|
|
37663
|
+
// Reusable custom schemas referenced by routes via meta.requestSchema/responseSchemas.
|
|
37664
|
+
refSchemas: /* @__PURE__ */ new Set(),
|
|
37665
|
+
usedTags: [],
|
|
37666
|
+
seenIds: /* @__PURE__ */ new Set()
|
|
37667
|
+
};
|
|
37668
|
+
for (const route of routes) {
|
|
37669
|
+
buildOperation(route, spec, ctx);
|
|
37670
|
+
}
|
|
37671
|
+
buildRefSchemas(spec, ctx.refSchemas);
|
|
37672
|
+
buildTags(spec, ctx.usedTags);
|
|
37673
|
+
return spec;
|
|
37674
|
+
}
|
|
37675
|
+
function buildInfo() {
|
|
37638
37676
|
const info = {
|
|
37639
37677
|
title: process.env.TINA4_SWAGGER_TITLE ?? "Tina4 API",
|
|
37640
|
-
// The app's version, defaulting to 1.0.0 — NOT the framework's (Node shipped
|
|
37641
|
-
// 0.0.1). description defaults to the empty string, not a canned sentence.
|
|
37642
|
-
// Both are the settled cross-framework defaults (parity with the Python
|
|
37643
|
-
// master); TINA4_SWAGGER_VERSION / _DESCRIPTION still override.
|
|
37644
37678
|
version: process.env.TINA4_SWAGGER_VERSION ?? "1.0.0",
|
|
37645
37679
|
description: process.env.TINA4_SWAGGER_DESCRIPTION ?? ""
|
|
37646
37680
|
};
|
|
@@ -37657,135 +37691,134 @@ function generate2(routes, models = []) {
|
|
|
37657
37691
|
const [name, url] = licenseRaw.split("|").map((s) => s.trim());
|
|
37658
37692
|
info.license = url ? { name, url } : { name };
|
|
37659
37693
|
}
|
|
37660
|
-
|
|
37661
|
-
|
|
37662
|
-
|
|
37663
|
-
info,
|
|
37664
|
-
servers: resolveServers(),
|
|
37665
|
-
paths: {},
|
|
37666
|
-
components: {
|
|
37667
|
-
schemas: {},
|
|
37668
|
-
// Configurable security schemes (v3.13.42): bearerFormat via env, optional
|
|
37669
|
-
// apiKey scheme, plus any programmatically-registered schemes (which may
|
|
37670
|
-
// override bearerAuth — e.g. an oauth2 scheme with scopes).
|
|
37671
|
-
securitySchemes: schemes
|
|
37672
|
-
}
|
|
37673
|
-
};
|
|
37674
|
-
const defaultScheme = process.env.TINA4_SWAGGER_DEFAULT_SCHEME ?? "bearerAuth";
|
|
37675
|
-
const includePrefixes = csv(process.env.TINA4_SWAGGER_INCLUDE);
|
|
37676
|
-
const excludePrefixes = csv(process.env.TINA4_SWAGGER_EXCLUDE);
|
|
37677
|
-
const refSchemas = /* @__PURE__ */ new Set();
|
|
37678
|
-
const tableToSchema = /* @__PURE__ */ new Map();
|
|
37694
|
+
return info;
|
|
37695
|
+
}
|
|
37696
|
+
function buildComponentSchemas(models, spec, tableToSchema) {
|
|
37679
37697
|
for (const model of models) {
|
|
37680
37698
|
const schemaKey = schemaNameForModel(model);
|
|
37681
37699
|
tableToSchema.set(model.tableName, schemaKey);
|
|
37682
37700
|
spec.components.schemas[schemaKey] = modelToSchema(model);
|
|
37683
37701
|
}
|
|
37684
|
-
|
|
37685
|
-
|
|
37686
|
-
|
|
37687
|
-
|
|
37688
|
-
|
|
37689
|
-
|
|
37690
|
-
|
|
37691
|
-
|
|
37692
|
-
|
|
37693
|
-
|
|
37694
|
-
|
|
37695
|
-
|
|
37696
|
-
|
|
37697
|
-
|
|
37698
|
-
|
|
37699
|
-
|
|
37700
|
-
|
|
37701
|
-
|
|
37702
|
-
"200": { description: "Successful response" }
|
|
37703
|
-
}
|
|
37704
|
-
};
|
|
37705
|
-
if (route.meta?.description) operation.description = route.meta.description;
|
|
37706
|
-
if (route.meta?.deprecated) operation.deprecated = true;
|
|
37707
|
-
const pathParams = extractPathParams(route.pattern);
|
|
37708
|
-
if (pathParams.length > 0) {
|
|
37709
|
-
operation.parameters = pathParams.map(({ name, schema }) => ({
|
|
37710
|
-
name,
|
|
37711
|
-
in: "path",
|
|
37712
|
-
required: true,
|
|
37713
|
-
schema
|
|
37714
|
-
}));
|
|
37702
|
+
}
|
|
37703
|
+
function buildOperation(route, spec, ctx) {
|
|
37704
|
+
if (!isIncludedPath(route.pattern, ctx.includePrefixes, ctx.excludePrefixes)) return;
|
|
37705
|
+
const openApiPath = patternToOpenAPI(route.pattern);
|
|
37706
|
+
const method = route.method.toLowerCase();
|
|
37707
|
+
if (!spec.paths[openApiPath]) {
|
|
37708
|
+
spec.paths[openApiPath] = {};
|
|
37709
|
+
}
|
|
37710
|
+
const tags = route.meta?.tags ?? inferTags(route.pattern);
|
|
37711
|
+
for (const t of tags) {
|
|
37712
|
+
if (!ctx.usedTags.includes(t)) ctx.usedTags.push(t);
|
|
37713
|
+
}
|
|
37714
|
+
const operation = {
|
|
37715
|
+
operationId: uniqueOperationId(method, openApiPath, ctx.seenIds),
|
|
37716
|
+
summary: route.meta?.summary ?? `${route.method} ${route.pattern}`,
|
|
37717
|
+
tags,
|
|
37718
|
+
responses: route.meta?.responses ?? {
|
|
37719
|
+
"200": { description: "Successful response" }
|
|
37715
37720
|
}
|
|
37716
|
-
|
|
37717
|
-
|
|
37718
|
-
|
|
37719
|
-
|
|
37720
|
-
|
|
37721
|
-
|
|
37722
|
-
|
|
37723
|
-
|
|
37724
|
-
|
|
37725
|
-
|
|
37721
|
+
};
|
|
37722
|
+
if (route.meta?.description) operation.description = route.meta.description;
|
|
37723
|
+
if (route.meta?.deprecated) operation.deprecated = true;
|
|
37724
|
+
const parameters = operationParameters(route, method, ctx.models);
|
|
37725
|
+
if (parameters.length > 0) operation.parameters = parameters;
|
|
37726
|
+
operationRequestBody(route, method, operation, ctx);
|
|
37727
|
+
operationResponseSchemas(route, operation, ctx.refSchemas);
|
|
37728
|
+
operationSecurity(route, method, operation, ctx.schemes, ctx.defaultScheme);
|
|
37729
|
+
spec.paths[openApiPath][method] = operation;
|
|
37730
|
+
}
|
|
37731
|
+
function operationParameters(route, method, models) {
|
|
37732
|
+
let parameters = [];
|
|
37733
|
+
const pathParams = extractPathParams(route.pattern);
|
|
37734
|
+
if (pathParams.length > 0) {
|
|
37735
|
+
parameters = pathParams.map(({ name, schema }) => ({
|
|
37736
|
+
name,
|
|
37737
|
+
in: "path",
|
|
37738
|
+
required: true,
|
|
37739
|
+
schema
|
|
37740
|
+
}));
|
|
37741
|
+
}
|
|
37742
|
+
if (method === "get" && !route.pattern.includes("[id]") && !route.pattern.includes("[...")) {
|
|
37743
|
+
const modelName = inferModelFromPath(route.pattern);
|
|
37744
|
+
if (modelName && models.some((m) => m.tableName === modelName)) {
|
|
37745
|
+
parameters = [
|
|
37746
|
+
...parameters,
|
|
37747
|
+
{ name: "page", in: "query", schema: { type: "integer", default: 1 } },
|
|
37748
|
+
{ name: "limit", in: "query", schema: { type: "integer", default: 20 } },
|
|
37749
|
+
{ name: "sort", in: "query", schema: { type: "string" }, description: "Sort fields (prefix with - for descending)" }
|
|
37750
|
+
];
|
|
37726
37751
|
}
|
|
37727
|
-
|
|
37728
|
-
|
|
37729
|
-
|
|
37730
|
-
|
|
37731
|
-
|
|
37732
|
-
|
|
37733
|
-
|
|
37752
|
+
}
|
|
37753
|
+
return parameters;
|
|
37754
|
+
}
|
|
37755
|
+
function mediaWithExample(schema, example) {
|
|
37756
|
+
const media = { schema };
|
|
37757
|
+
if (example !== void 0) media.example = example;
|
|
37758
|
+
return media;
|
|
37759
|
+
}
|
|
37760
|
+
function operationRequestBody(route, method, operation, ctx) {
|
|
37761
|
+
const reqSchemaRef = parseRequestSchema(route.meta?.requestSchema);
|
|
37762
|
+
if (reqSchemaRef && (method === "post" || method === "put" || method === "patch")) {
|
|
37763
|
+
ctx.refSchemas.add(reqSchemaRef.name);
|
|
37764
|
+
const media = mediaWithExample({ $ref: `#/components/schemas/${reqSchemaRef.name}` }, route.meta?.example);
|
|
37765
|
+
operation.requestBody = {
|
|
37766
|
+
content: { [reqSchemaRef.contentType]: media }
|
|
37767
|
+
};
|
|
37768
|
+
} else if (method === "post" || method === "put") {
|
|
37769
|
+
const modelName = inferModelFromPath(route.pattern);
|
|
37770
|
+
const schemaKey = modelName ? ctx.tableToSchema.get(modelName) : void 0;
|
|
37771
|
+
if (schemaKey) {
|
|
37772
|
+
const sref = `#/components/schemas/${schemaKey}`;
|
|
37734
37773
|
operation.requestBody = {
|
|
37735
|
-
|
|
37774
|
+
required: true,
|
|
37775
|
+
content: { "application/json": mediaWithExample({ $ref: sref }, route.meta?.example) }
|
|
37736
37776
|
};
|
|
37737
|
-
|
|
37738
|
-
|
|
37739
|
-
|
|
37740
|
-
if (schemaKey) {
|
|
37741
|
-
const sref = `#/components/schemas/${schemaKey}`;
|
|
37742
|
-
const media = { schema: { $ref: sref } };
|
|
37743
|
-
if (route.meta?.example !== void 0) media.example = route.meta.example;
|
|
37744
|
-
operation.requestBody = {
|
|
37745
|
-
required: true,
|
|
37746
|
-
content: { "application/json": media }
|
|
37747
|
-
};
|
|
37748
|
-
if (route.meta?.responses === void 0) {
|
|
37749
|
-
operation.responses = {
|
|
37750
|
-
"200": { description: "Successful response", content: { "application/json": { schema: { $ref: sref } } } }
|
|
37751
|
-
};
|
|
37752
|
-
}
|
|
37753
|
-
} else if (route.meta?.example !== void 0) {
|
|
37754
|
-
operation.requestBody = {
|
|
37755
|
-
content: { "application/json": { schema: inferSchema(route.meta.example), example: route.meta.example } }
|
|
37777
|
+
if (route.meta?.responses === void 0) {
|
|
37778
|
+
operation.responses = {
|
|
37779
|
+
"200": { description: "Successful response", content: { "application/json": { schema: { $ref: sref } } } }
|
|
37756
37780
|
};
|
|
37757
37781
|
}
|
|
37782
|
+
} else if (route.meta?.example !== void 0) {
|
|
37783
|
+
operation.requestBody = {
|
|
37784
|
+
content: { "application/json": { schema: inferSchema(route.meta.example), example: route.meta.example } }
|
|
37785
|
+
};
|
|
37758
37786
|
}
|
|
37759
|
-
|
|
37760
|
-
|
|
37761
|
-
|
|
37762
|
-
|
|
37763
|
-
|
|
37764
|
-
|
|
37765
|
-
|
|
37766
|
-
|
|
37767
|
-
|
|
37768
|
-
|
|
37769
|
-
|
|
37770
|
-
|
|
37787
|
+
}
|
|
37788
|
+
}
|
|
37789
|
+
function operationResponseSchemas(route, operation, refSchemas) {
|
|
37790
|
+
const respSchemas = parseResponseSchemas(route.meta?.responseSchemas);
|
|
37791
|
+
if (respSchemas.length > 0) {
|
|
37792
|
+
const responses = operation.responses;
|
|
37793
|
+
for (const { status: status2, name, isList } of respSchemas) {
|
|
37794
|
+
refSchemas.add(name);
|
|
37795
|
+
const sref = `#/components/schemas/${name}`;
|
|
37796
|
+
const schema = isList ? { type: "array", items: { $ref: sref } } : { $ref: sref };
|
|
37797
|
+
responses[status2] = {
|
|
37798
|
+
description: status2.startsWith("2") ? "Successful response" : "Response",
|
|
37799
|
+
content: { "application/json": { schema } }
|
|
37800
|
+
};
|
|
37771
37801
|
}
|
|
37772
|
-
|
|
37773
|
-
|
|
37774
|
-
|
|
37775
|
-
|
|
37776
|
-
|
|
37777
|
-
|
|
37778
|
-
|
|
37779
|
-
|
|
37780
|
-
} else if (routeRequiresAuth(route, method)) {
|
|
37781
|
-
const requirements = [{ [defaultScheme]: [] }];
|
|
37782
|
-
if (defaultScheme === "bearerAuth" && schemes.ssoSession) requirements.push({ ssoSession: [] });
|
|
37783
|
-
operation.security = sanitizeSecurity(requirements, schemes);
|
|
37802
|
+
}
|
|
37803
|
+
}
|
|
37804
|
+
function operationSecurity(route, method, operation, schemes, defaultScheme) {
|
|
37805
|
+
const hasExplicitSecurity = route.meta?.security !== void 0 || route.meta?.scopes !== void 0 && route.meta.scopes.length > 0;
|
|
37806
|
+
if (hasExplicitSecurity) {
|
|
37807
|
+
const normalized = normalizeSecurity(route.meta?.security, route.meta?.scopes);
|
|
37808
|
+
operation.security = normalized.length > 0 ? sanitizeSecurity(normalized, schemes) : [];
|
|
37809
|
+
if (normalized.length > 0) {
|
|
37784
37810
|
const responses = operation.responses;
|
|
37785
37811
|
if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
|
|
37786
37812
|
}
|
|
37787
|
-
|
|
37813
|
+
} else if (routeRequiresAuth(route, method)) {
|
|
37814
|
+
const requirements = [{ [defaultScheme]: [] }];
|
|
37815
|
+
if (defaultScheme === "bearerAuth" && schemes.ssoSession) requirements.push({ ssoSession: [] });
|
|
37816
|
+
operation.security = sanitizeSecurity(requirements, schemes);
|
|
37817
|
+
const responses = operation.responses;
|
|
37818
|
+
if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
|
|
37788
37819
|
}
|
|
37820
|
+
}
|
|
37821
|
+
function buildRefSchemas(spec, refSchemas) {
|
|
37789
37822
|
if (refSchemas.size > 0) {
|
|
37790
37823
|
const schemas = spec.components.schemas;
|
|
37791
37824
|
for (const name of refSchemas) {
|
|
@@ -37794,10 +37827,11 @@ function generate2(routes, models = []) {
|
|
|
37794
37827
|
}
|
|
37795
37828
|
}
|
|
37796
37829
|
}
|
|
37830
|
+
}
|
|
37831
|
+
function buildTags(spec, usedTags) {
|
|
37797
37832
|
if (usedTags.length > 0) {
|
|
37798
37833
|
spec.tags = usedTags.map((name) => ({ name }));
|
|
37799
37834
|
}
|
|
37800
|
-
return spec;
|
|
37801
37835
|
}
|
|
37802
37836
|
function routeRequiresAuth(route, method) {
|
|
37803
37837
|
if (route.noAuth) return false;
|