tina4-nodejs 3.13.137 → 3.13.139
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 +33 -3
- package/COMMERCIAL-LICENSE.md +8 -0
- package/LICENSE +373 -0
- package/NOTICE +11 -0
- package/README.md +24 -2
- package/package.json +44 -6
- package/packages/cli/dist/bin.js +19979 -18102
- package/packages/cli/src/bin.ts +8 -0
- package/packages/cli/src/commands/build.ts +8 -0
- package/packages/cli/src/commands/generate.ts +8 -0
- package/packages/cli/src/commands/init.ts +8 -0
- package/packages/cli/src/commands/lint.ts +8 -0
- package/packages/cli/src/commands/migrate.ts +8 -0
- package/packages/cli/src/commands/migrateCreate.ts +8 -0
- package/packages/cli/src/commands/migrateRollback.ts +8 -0
- package/packages/cli/src/commands/migrateStatus.ts +8 -0
- package/packages/cli/src/commands/queue.ts +8 -0
- package/packages/cli/src/commands/routes.ts +8 -0
- package/packages/cli/src/commands/seed.ts +8 -0
- package/packages/cli/src/commands/serve.ts +11 -0
- package/packages/cli/src/commands/test.ts +8 -0
- package/packages/core/dist/index.js +19976 -18097
- package/packages/core/gallery/auth/src/routes/api/gallery/auth/login/post.ts +8 -0
- package/packages/core/gallery/auth/src/routes/api/gallery/auth/verify/get.ts +8 -0
- package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +8 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/notes/get.ts +8 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/notes/post.ts +8 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/tables/get.ts +8 -0
- package/packages/core/gallery/error-overlay/src/routes/api/gallery/crash/get.ts +8 -0
- package/packages/core/gallery/orm/src/routes/api/gallery/products/get.ts +8 -0
- package/packages/core/gallery/orm/src/routes/api/gallery/products/post.ts +8 -0
- package/packages/core/gallery/queue/src/lib/queueDb.ts +8 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/consume/post.ts +8 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/fail/post.ts +8 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/produce/post.ts +8 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/retry/post.ts +8 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/status/get.ts +8 -0
- package/packages/core/gallery/queue/src/routes/gallery/queue/get.ts +8 -0
- package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/get.ts +8 -0
- package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/post.ts +8 -0
- package/packages/core/gallery/templates/src/routes/gallery/page/get.ts +8 -0
- package/packages/core/gallery/templates/src/templates/gallery_page.twig +8 -0
- package/packages/core/public/THIRD-PARTY-NOTICES.md +55 -0
- package/packages/core/public/bundled-component-provenance.json +48 -0
- package/packages/core/public/js/tina4js.min.js +13 -3
- package/packages/core/public/licenses/swagger-ui-3.10.0-original-notice.txt +11 -0
- package/packages/core/public/licenses/swagger-ui-5.17.14-NOTICE.txt +2 -0
- package/packages/core/public/licenses/swagger-ui-Apache-2.0.txt +202 -0
- package/packages/core/public/licenses/tina4-css-MIT.txt +21 -0
- package/packages/core/public/licenses/tina4-dev-admin-components.json +240 -0
- package/packages/core/public/licenses/tina4-dev-admin-third-party.txt +919 -0
- package/packages/core/public/licenses/tina4-js-MIT.txt +20 -0
- package/packages/core/src/__feedback/widget.js +8 -0
- package/packages/core/src/_missing.ts +8 -0
- package/packages/core/src/ai.ts +8 -0
- package/packages/core/src/aiClient.ts +8 -0
- package/packages/core/src/api.ts +48 -7
- package/packages/core/src/auth.ts +94 -14
- package/packages/core/src/authGate.ts +35 -20
- package/packages/core/src/background.ts +8 -0
- package/packages/core/src/cache.ts +43 -4
- package/packages/core/src/constants.ts +8 -0
- package/packages/core/src/container.ts +8 -0
- package/packages/core/src/context/chunker.ts +8 -0
- package/packages/core/src/context/index.ts +8 -0
- package/packages/core/src/devAdmin.ts +185 -59
- package/packages/core/src/devMailbox.ts +8 -0
- package/packages/core/src/dispatchPipeline.ts +8 -0
- package/packages/core/src/docs.ts +8 -0
- package/packages/core/src/docsAutoDiscovery.ts +8 -0
- package/packages/core/src/docsParser.ts +8 -0
- package/packages/core/src/docsScanner.ts +44 -0
- package/packages/core/src/docsSignatures.ts +8 -0
- package/packages/core/src/dotenv.ts +8 -0
- package/packages/core/src/env.ts +8 -0
- package/packages/core/src/errorOverlay.ts +8 -0
- package/packages/core/src/events.ts +8 -0
- package/packages/core/src/fakeData.ts +8 -0
- package/packages/core/src/feedback.ts +8 -0
- package/packages/core/src/graphql.ts +108 -5
- package/packages/core/src/health.ts +8 -0
- package/packages/core/src/htmlElement.ts +8 -0
- package/packages/core/src/i18n.ts +8 -0
- package/packages/core/src/index.ts +9 -0
- package/packages/core/src/job.ts +16 -1
- package/packages/core/src/logger.ts +8 -0
- package/packages/core/src/mcp.test.ts +8 -0
- package/packages/core/src/mcp.ts +11 -1
- package/packages/core/src/messenger.ts +143 -86
- package/packages/core/src/metrics.ts +13 -0
- package/packages/core/src/middleware.ts +1181 -1097
- package/packages/core/src/mqtt.ts +8 -0
- package/packages/core/src/mqttMessage.ts +8 -0
- package/packages/core/src/optionalPackage.ts +48 -0
- package/packages/core/src/plan.ts +8 -0
- package/packages/core/src/portTakeover.ts +8 -0
- package/packages/core/src/projectIndex.ts +8 -0
- package/packages/core/src/projectIndexExtractors.ts +8 -0
- package/packages/core/src/projectIndexStorage.ts +8 -0
- package/packages/core/src/push.ts +24 -0
- package/packages/core/src/queue.ts +23 -2
- package/packages/core/src/queueBackends/kafkaBackend.ts +8 -0
- package/packages/core/src/queueBackends/liteBackend.ts +23 -0
- package/packages/core/src/queueBackends/mongoBackend.ts +61 -14
- package/packages/core/src/queueBackends/rabbitmqBackend.ts +8 -0
- package/packages/core/src/rateLimiter.ts +8 -0
- package/packages/core/src/request.ts +59 -10
- package/packages/core/src/response.ts +79 -5
- package/packages/core/src/routeDiscovery.ts +8 -0
- package/packages/core/src/router.ts +8 -0
- package/packages/core/src/server.ts +79 -25
- package/packages/core/src/service.ts +8 -0
- package/packages/core/src/session.ts +8 -0
- package/packages/core/src/sessionHandlers/childError.ts +8 -0
- package/packages/core/src/sessionHandlers/databaseHandler.ts +68 -3
- package/packages/core/src/sessionHandlers/memcachedHandler.ts +8 -0
- package/packages/core/src/sessionHandlers/mongoClient.ts +8 -0
- package/packages/core/src/sessionHandlers/mongoHandler.ts +8 -0
- package/packages/core/src/sessionHandlers/respClient.ts +8 -0
- package/packages/core/src/sessionHandlers/sqlClient.ts +8 -0
- package/packages/core/src/sessionHandlers/syncBridge.ts +8 -0
- package/packages/core/src/sessionHandlers/syncSocket.ts +8 -0
- package/packages/core/src/sessionHandlers/valkeyHandler.ts +8 -0
- package/packages/core/src/sso.ts +20 -4
- package/packages/core/src/ssoIdentity.ts +19 -0
- package/packages/core/src/ssrf.ts +221 -0
- package/packages/core/src/static.ts +8 -0
- package/packages/core/src/test.ts +8 -0
- package/packages/core/src/testClient.ts +11 -0
- package/packages/core/src/testing.ts +8 -0
- package/packages/core/src/transport.ts +369 -0
- package/packages/core/src/trustedProxy.ts +8 -0
- package/packages/core/src/types.ts +13 -0
- package/packages/core/src/validator.ts +8 -0
- package/packages/core/src/version.ts +8 -0
- package/packages/core/src/websocket.ts +12 -2
- package/packages/core/src/websocketBackplane.ts +49 -32
- package/packages/core/src/websocketConnection.ts +8 -0
- package/packages/core/src/wsdl.ts +197 -115
- package/packages/core/templates/errors/302.twig +8 -0
- package/packages/core/templates/errors/401.twig +8 -0
- package/packages/core/templates/errors/403.twig +8 -0
- package/packages/core/templates/errors/404.twig +8 -0
- package/packages/core/templates/errors/500.twig +8 -0
- package/packages/core/templates/errors/502.twig +8 -0
- package/packages/core/templates/errors/503.twig +8 -0
- package/packages/core/templates/errors/base.twig +8 -0
- package/packages/frond/dist/index.js +157 -17
- package/packages/frond/src/engine.ts +186 -17
- package/packages/frond/src/index.ts +8 -0
- package/packages/orm/dist/index.js +8641 -6765
- package/packages/orm/src/adapters/firebird.ts +50 -3
- package/packages/orm/src/adapters/mongodb.ts +8 -0
- package/packages/orm/src/adapters/mssql.ts +29 -7
- package/packages/orm/src/adapters/mysql.ts +17 -1
- package/packages/orm/src/adapters/odbc.ts +11 -0
- package/packages/orm/src/adapters/pg-types.d.ts +8 -0
- package/packages/orm/src/adapters/postgres.ts +34 -8
- package/packages/orm/src/adapters/sqlDialect.ts +57 -0
- package/packages/orm/src/adapters/sqlite.ts +26 -0
- package/packages/orm/src/autoCrud.ts +78 -36
- package/packages/orm/src/baseModel.ts +61 -29
- package/packages/orm/src/cachedDatabase.ts +67 -1
- package/packages/orm/src/connectTimeout.ts +8 -0
- package/packages/orm/src/database.ts +649 -401
- package/packages/orm/src/databaseResult.ts +8 -0
- package/packages/orm/src/databaseUrl.ts +8 -0
- package/packages/orm/src/docstore.ts +23 -1
- package/packages/orm/src/fakeData.ts +8 -0
- package/packages/orm/src/graph/adapters/arango.ts +8 -0
- package/packages/orm/src/graph/adapters/bolt.ts +8 -0
- package/packages/orm/src/graph/adapters/ultipa.ts +8 -0
- package/packages/orm/src/graph/connectTimeout.ts +8 -0
- package/packages/orm/src/graph/errors.ts +8 -0
- package/packages/orm/src/graph/graphAdapter.ts +8 -0
- package/packages/orm/src/graph/graphDatabase.ts +8 -0
- package/packages/orm/src/graph/graphUrl.ts +10 -1
- package/packages/orm/src/graph/shapes.ts +8 -0
- package/packages/orm/src/index.ts +147 -139
- package/packages/orm/src/migration.ts +8 -0
- package/packages/orm/src/model.ts +8 -0
- package/packages/orm/src/modelCollection.ts +8 -0
- package/packages/orm/src/point.ts +8 -0
- package/packages/orm/src/query.ts +149 -33
- package/packages/orm/src/queryBuilder.ts +8 -0
- package/packages/orm/src/realtime/index.ts +8 -0
- package/packages/orm/src/realtime/models/attachment.ts +8 -0
- package/packages/orm/src/realtime/models/channel.ts +8 -0
- package/packages/orm/src/realtime/models/channelMember.ts +8 -0
- package/packages/orm/src/realtime/models/message.ts +8 -0
- package/packages/orm/src/realtime/models/workspace.ts +8 -0
- package/packages/orm/src/realtime/realtime.ts +8 -0
- package/packages/orm/src/realtime/storage.ts +43 -12
- package/packages/orm/src/seeder.ts +8 -0
- package/packages/orm/src/seederTable.ts +8 -0
- package/packages/orm/src/seederTypes.ts +8 -0
- package/packages/orm/src/sqlTranslator.ts +185 -8
- package/packages/orm/src/types.ts +15 -0
- package/packages/orm/src/validation.ts +8 -0
- package/packages/swagger/src/generator.ts +8 -0
- package/packages/swagger/src/index.ts +8 -0
- package/packages/swagger/src/ui.ts +8 -0
- package/types/core/src/api.d.ts +6 -0
- package/types/core/src/auth.d.ts +30 -0
- package/types/core/src/background.d.ts +0 -16
- package/types/core/src/cache.d.ts +6 -0
- package/types/core/src/devAdmin.d.ts +18 -12
- package/types/core/src/docs.d.ts +0 -17
- package/types/core/src/errorOverlay.d.ts +0 -24
- package/types/core/src/events.d.ts +0 -12
- package/types/core/src/feedback.d.ts +0 -23
- package/types/core/src/graphql.d.ts +25 -21
- package/types/core/src/htmlElement.d.ts +0 -15
- package/types/core/src/index.d.ts +1 -0
- package/types/core/src/job.d.ts +1 -0
- package/types/core/src/messenger.d.ts +12 -1
- package/types/core/src/metrics.d.ts +2 -0
- package/types/core/src/middleware.d.ts +45 -1
- package/types/core/src/mqtt.d.ts +0 -33
- package/types/core/src/optionalPackage.d.ts +25 -0
- package/types/core/src/plan.d.ts +0 -10
- package/types/core/src/projectIndex.d.ts +0 -10
- package/types/core/src/push.d.ts +5 -0
- package/types/core/src/queue.d.ts +7 -1
- package/types/core/src/queueBackends/liteBackend.d.ts +9 -0
- package/types/core/src/queueBackends/mongoBackend.d.ts +10 -1
- package/types/core/src/request.d.ts +12 -0
- package/types/core/src/response.d.ts +13 -0
- package/types/core/src/server.d.ts +19 -0
- package/types/core/src/sessionHandlers/childError.d.ts +0 -15
- package/types/core/src/sessionHandlers/databaseHandler.d.ts +25 -1
- package/types/core/src/sso.d.ts +1 -0
- package/types/core/src/ssoIdentity.d.ts +10 -0
- package/types/core/src/ssrf.d.ts +19 -0
- package/types/core/src/transport.d.ts +28 -0
- package/types/core/src/trustedProxy.d.ts +0 -16
- package/types/core/src/types.d.ts +5 -0
- package/types/core/src/websocketBackplane.d.ts +1 -0
- package/types/core/src/wsdl.d.ts +8 -22
- package/types/frond/src/engine.d.ts +7 -1
- package/types/orm/src/adapters/firebird.d.ts +12 -0
- package/types/orm/src/adapters/mssql.d.ts +11 -0
- package/types/orm/src/adapters/mysql.d.ts +2 -0
- package/types/orm/src/adapters/postgres.d.ts +11 -0
- package/types/orm/src/adapters/sqlDialect.d.ts +25 -0
- package/types/orm/src/adapters/sqlite.d.ts +7 -0
- package/types/orm/src/baseModel.d.ts +5 -0
- package/types/orm/src/cachedDatabase.d.ts +27 -0
- package/types/orm/src/database.d.ts +46 -7
- package/types/orm/src/graph/graphUrl.d.ts +0 -8
- package/types/orm/src/index.d.ts +1 -1
- package/types/orm/src/query.d.ts +48 -2
- package/types/orm/src/realtime/storage.d.ts +2 -1
- package/types/orm/src/sqlTranslator.d.ts +70 -18
- package/types/orm/src/types.d.ts +6 -0
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { FieldDefinition, QueryOptions } from "./types.js";
|
|
2
10
|
import { DEFAULT_ROW_CAP } from "./database.js";
|
|
11
|
+
import { quoteIdentifierAnsi } from "./adapters/sqlDialect.js";
|
|
3
12
|
|
|
4
13
|
export interface ParsedQuery {
|
|
5
14
|
where: string;
|
|
@@ -9,14 +18,103 @@ export interface ParsedQuery {
|
|
|
9
18
|
params: unknown[];
|
|
10
19
|
}
|
|
11
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a caller-supplied key to the model's DB column, or null when the key
|
|
23
|
+
* is not a declared field.
|
|
24
|
+
*
|
|
25
|
+
* tina4: ADR-0069 - identifiers that reach SQL come from the model, never the
|
|
26
|
+
* request. The key may be a declared field (property) name or that field's
|
|
27
|
+
* column name; either way the column returned is the model's own, so nothing
|
|
28
|
+
* but a declared column can be emitted. `columnOf` is the caller's existing
|
|
29
|
+
* property->column mapping (BaseModel.getDbColumn / AutoCrud's getDbCol), so
|
|
30
|
+
* the mapping rule lives in one place. Own keys only, so an inherited Object
|
|
31
|
+
* property can never resolve. Used by AutoCrud (filter + sort) and by
|
|
32
|
+
* BaseModel.find(object).
|
|
33
|
+
*/
|
|
34
|
+
export function resolveFieldColumn(
|
|
35
|
+
fields: Record<string, FieldDefinition>,
|
|
36
|
+
columnOf: (field: string) => string,
|
|
37
|
+
key: string,
|
|
38
|
+
): string | null {
|
|
39
|
+
const field = resolveField(fields, columnOf, key);
|
|
40
|
+
return field === null ? null : columnOf(field);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The declared FIELD (property) name a key names - by the field name itself or
|
|
45
|
+
* by its column - or null. The write-side twin of resolveFieldColumn(): AutoCrud
|
|
46
|
+
* writes need the field, to validate the body and then map it to its column.
|
|
47
|
+
*/
|
|
48
|
+
export function resolveField(
|
|
49
|
+
fields: Record<string, FieldDefinition>,
|
|
50
|
+
columnOf: (field: string) => string,
|
|
51
|
+
key: string,
|
|
52
|
+
): string | null {
|
|
53
|
+
if (Object.hasOwn(fields, key)) return key;
|
|
54
|
+
for (const field of Object.keys(fields)) {
|
|
55
|
+
if (columnOf(field) === key) return field;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A filter or sort key that does not resolve to a declared model field. */
|
|
61
|
+
export class UnknownFieldError extends Error {
|
|
62
|
+
readonly kind: "filter" | "sort";
|
|
63
|
+
readonly field: string;
|
|
64
|
+
|
|
65
|
+
constructor(kind: "filter" | "sort", field: string) {
|
|
66
|
+
super(`Unknown ${kind} field '${field}'`);
|
|
67
|
+
this.name = "UnknownFieldError";
|
|
68
|
+
this.kind = kind;
|
|
69
|
+
this.field = field;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A query parameter whose SHAPE is wrong (a list or map where one value belongs). */
|
|
74
|
+
export class InvalidQueryParameterError extends Error {
|
|
75
|
+
constructor(message: string) {
|
|
76
|
+
super(message);
|
|
77
|
+
this.name = "InvalidQueryParameterError";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const filterValueError = (field: string): InvalidQueryParameterError =>
|
|
82
|
+
new InvalidQueryParameterError(`Filter value for '${field}' must be a single value`);
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build the list SQL from parsed query options.
|
|
86
|
+
*
|
|
87
|
+
* `quote` renders every table/column name the builder emits - pass the bound
|
|
88
|
+
* adapter's dialect (`(name) => quoteIdentifier(adapter, name)`); the default is
|
|
89
|
+
* the ANSI `"name"`.
|
|
90
|
+
*
|
|
91
|
+
* Every filter key and sort field is passed through `resolveColumn` (see
|
|
92
|
+
* resolveFieldColumn) and only the column it returns is emitted; a key it does
|
|
93
|
+
* not resolve throws UnknownFieldError before any SQL exists. With no
|
|
94
|
+
* resolver, no filter or sort key resolves (ADR-0069).
|
|
95
|
+
*/
|
|
12
96
|
export function buildQuery(
|
|
13
97
|
tableName: string,
|
|
14
98
|
options: QueryOptions,
|
|
15
99
|
extraConditions?: string[],
|
|
16
|
-
|
|
100
|
+
quote: (name: string) => string = quoteIdentifierAnsi,
|
|
101
|
+
resolveColumn: (key: string) => string | null = () => null,
|
|
102
|
+
): {
|
|
103
|
+
sql: string; countSql: string; params: unknown[]; limit: number; offset: number; page: number;
|
|
104
|
+
/** The same SELECT without LIMIT/OFFSET, for adapterFetch to page in the engine's own syntax. */
|
|
105
|
+
pageSql: string;
|
|
106
|
+
/** The filter parameters alone (params minus the trailing limit/offset). */
|
|
107
|
+
filterParams: unknown[];
|
|
108
|
+
} {
|
|
17
109
|
const conditions: string[] = [];
|
|
18
110
|
const params: unknown[] = [];
|
|
19
111
|
|
|
112
|
+
const column = (kind: "filter" | "sort", key: string): string => {
|
|
113
|
+
const resolved = resolveColumn(key);
|
|
114
|
+
if (resolved === null) throw new UnknownFieldError(kind, key);
|
|
115
|
+
return quote(resolved);
|
|
116
|
+
};
|
|
117
|
+
|
|
20
118
|
// Add extra conditions (soft delete, table filter)
|
|
21
119
|
if (extraConditions) {
|
|
22
120
|
conditions.push(...extraConditions);
|
|
@@ -25,19 +123,20 @@ export function buildQuery(
|
|
|
25
123
|
// Parse filters
|
|
26
124
|
if (options.filter) {
|
|
27
125
|
for (const [field, value] of Object.entries(options.filter)) {
|
|
126
|
+
const col = column("filter", field);
|
|
28
127
|
if (typeof value === "object" && value !== null) {
|
|
29
128
|
// Operator filters: filter[age][gt]=25
|
|
30
129
|
const ops = value as Record<string, unknown>;
|
|
31
130
|
for (const [op, opVal] of Object.entries(ops)) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
131
|
+
// Own keys only: an inherited name ("constructor") is not an operator,
|
|
132
|
+
// and an unknown operator is an error, never silently dropped.
|
|
133
|
+
if (!Object.hasOwn(operatorMap, op)) throw filterValueError(field);
|
|
134
|
+
conditions.push(`${col} ${operatorMap[op]} ?`);
|
|
135
|
+
params.push(opVal);
|
|
37
136
|
}
|
|
38
137
|
} else {
|
|
39
138
|
// Exact match: filter[name]=John
|
|
40
|
-
conditions.push(
|
|
139
|
+
conditions.push(`${col} = ?`);
|
|
41
140
|
params.push(value);
|
|
42
141
|
}
|
|
43
142
|
}
|
|
@@ -45,17 +144,19 @@ export function buildQuery(
|
|
|
45
144
|
|
|
46
145
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
47
146
|
|
|
48
|
-
// Sort
|
|
147
|
+
// Sort: comma-separated parts, empty parts skipped, leading "-" = DESC.
|
|
148
|
+
// ORDER BY is built only from resolved columns and the words ASC / DESC.
|
|
49
149
|
let orderClause = "";
|
|
50
150
|
if (options.sort) {
|
|
51
|
-
const parts =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
151
|
+
const parts: string[] = [];
|
|
152
|
+
for (const part of options.sort.split(",")) {
|
|
153
|
+
const trimmed = part.trim();
|
|
154
|
+
if (trimmed === "") continue;
|
|
155
|
+
const descending = trimmed.startsWith("-");
|
|
156
|
+
const col = column("sort", descending ? trimmed.slice(1) : trimmed);
|
|
157
|
+
parts.push(`${col} ${descending ? "DESC" : "ASC"}`);
|
|
158
|
+
}
|
|
159
|
+
if (parts.length > 0) orderClause = `ORDER BY ${parts.join(", ")}`;
|
|
59
160
|
}
|
|
60
161
|
|
|
61
162
|
// Pagination — PAGE-DEC-01: clamp page >= 1 BEFORE deriving offset, so
|
|
@@ -71,8 +172,9 @@ export function buildQuery(
|
|
|
71
172
|
const page = Math.max(options.page ?? 1, 1);
|
|
72
173
|
const offset = (page - 1) * limit;
|
|
73
174
|
|
|
74
|
-
const
|
|
75
|
-
const
|
|
175
|
+
const pageSql = `SELECT * FROM ${quote(tableName)} ${whereClause} ${orderClause}`.trimEnd();
|
|
176
|
+
const sql = `SELECT * FROM ${quote(tableName)} ${whereClause} ${orderClause} LIMIT ? OFFSET ?`;
|
|
177
|
+
const countSql = `SELECT COUNT(*) as total FROM ${quote(tableName)} ${whereClause}`;
|
|
76
178
|
|
|
77
179
|
return {
|
|
78
180
|
sql,
|
|
@@ -81,28 +183,42 @@ export function buildQuery(
|
|
|
81
183
|
limit,
|
|
82
184
|
offset,
|
|
83
185
|
page,
|
|
186
|
+
pageSql,
|
|
187
|
+
filterParams: params,
|
|
84
188
|
};
|
|
85
189
|
}
|
|
86
190
|
|
|
87
191
|
export function parseQueryString(query: Record<string, string>): QueryOptions {
|
|
88
192
|
const options: QueryOptions = {};
|
|
89
193
|
|
|
90
|
-
// Parse filter params: filter[name]=John or filter[age][gt]=25
|
|
91
|
-
|
|
194
|
+
// Parse filter params: filter[name]=John or filter[age][gt]=25. ANY key
|
|
195
|
+
// inside the brackets is captured - buildQuery rejects one that is not a
|
|
196
|
+
// declared field rather than it being silently ignored here (ADR-0069).
|
|
197
|
+
// A second bracket must be a known operator; a list (filter[name][]), a map
|
|
198
|
+
// (filter[name][x]) or a nested key is a wrong-shaped value and throws
|
|
199
|
+
// InvalidQueryParameterError. `sort[...]` is a list/map where a single
|
|
200
|
+
// comma-separated string belongs, and throws the same way.
|
|
201
|
+
// A null-prototype map, so a "__proto__" key is stored (and then rejected)
|
|
202
|
+
// instead of silently re-pointing the object's prototype.
|
|
203
|
+
const filter: Record<string, unknown> = Object.create(null);
|
|
92
204
|
for (const [key, value] of Object.entries(query)) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
205
|
+
if (key.startsWith("sort[")) {
|
|
206
|
+
throw new InvalidQueryParameterError("Query parameter 'sort' must be a single comma-separated string");
|
|
207
|
+
}
|
|
208
|
+
if (!key.startsWith("filter[")) continue;
|
|
209
|
+
const filterMatch = key.match(/^filter\[([^\]]*)\](.*)$/s);
|
|
210
|
+
if (!filterMatch) throw new UnknownFieldError("filter", key.slice("filter[".length));
|
|
211
|
+
const [, field, rest] = filterMatch;
|
|
212
|
+
if (rest === "") {
|
|
213
|
+
filter[field] = value;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const operator = rest.match(/^\[([^[\]]*)\]$/)?.[1];
|
|
217
|
+
if (operator === undefined || !Object.hasOwn(operatorMap, operator)) throw filterValueError(field);
|
|
218
|
+
if (!filter[field] || typeof filter[field] !== "object") {
|
|
219
|
+
filter[field] = {};
|
|
105
220
|
}
|
|
221
|
+
(filter[field] as Record<string, string>)[operator] = value;
|
|
106
222
|
}
|
|
107
223
|
if (Object.keys(filter).length > 0) {
|
|
108
224
|
options.filter = filter;
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
/**
|
|
2
10
|
* QueryBuilder — Fluent SQL query builder for Tina4 Node.js.
|
|
3
11
|
*
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
export { realtime, iceServers, type RealtimeOptions } from "./realtime.js";
|
|
2
10
|
export {
|
|
3
11
|
LocalStorage,
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
import { BaseModel } from "../../baseModel.js";
|
|
2
10
|
|
|
3
11
|
/**
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
import { BaseModel } from "../../baseModel.js";
|
|
2
10
|
|
|
3
11
|
/**
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
import { BaseModel } from "../../baseModel.js";
|
|
2
10
|
|
|
3
11
|
/**
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
import { BaseModel } from "../../baseModel.js";
|
|
2
10
|
|
|
3
11
|
/**
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
import { BaseModel } from "../../baseModel.js";
|
|
2
10
|
|
|
3
11
|
/**
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
import { createHmac } from "node:crypto";
|
|
2
10
|
import {
|
|
3
11
|
Router,
|
|
@@ -1,14 +1,41 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
import { randomBytes } from "node:crypto";
|
|
2
10
|
import { mkdirSync, readFileSync, writeFileSync, unlinkSync, statSync } from "node:fs";
|
|
3
11
|
import { resolve, sep } from "node:path";
|
|
4
12
|
import { createRequire } from "node:module";
|
|
5
13
|
import { Log } from "../../../core/src/index.js";
|
|
14
|
+
import { optionalPackageMissing } from "../../../core/src/optionalPackage.js";
|
|
6
15
|
|
|
7
16
|
// ESM has no global `require`; create one so the OPTIONAL aws-sdk can be loaded
|
|
8
|
-
// synchronously in S3Storage's constructor.
|
|
9
|
-
// selectStorage() catches it, degrading to local storage.
|
|
17
|
+
// synchronously in S3Storage's constructor.
|
|
10
18
|
const require = createRequire(import.meta.url);
|
|
11
19
|
|
|
20
|
+
/** S3 needs both SDK packages, so a missing one names the command for both. */
|
|
21
|
+
const S3_PACKAGES = ["@aws-sdk/client-s3", "@aws-sdk/s3-request-presigner"];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Load an @aws-sdk package (an optional peer, ADR-0067). A missing one throws
|
|
25
|
+
* the actionable install error; selectStorage() catches it and logs it while
|
|
26
|
+
* degrading to local storage. Any OTHER load failure is re-thrown untouched,
|
|
27
|
+
* so an installed-but-broken SDK is never mislabelled "not installed".
|
|
28
|
+
*/
|
|
29
|
+
function loadAwsSdk(packageName: string): any {
|
|
30
|
+
try {
|
|
31
|
+
return require(packageName);
|
|
32
|
+
} catch (error: any) {
|
|
33
|
+
const missing = error?.code === "MODULE_NOT_FOUND" && String(error?.message ?? "").includes(`'${packageName}'`);
|
|
34
|
+
if (!missing) throw error;
|
|
35
|
+
throw optionalPackageMissing("S3Storage", S3_PACKAGES);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
12
39
|
/**
|
|
13
40
|
* Pluggable file storage for the realtime "files" feature. StorageBackend is
|
|
14
41
|
* the interface; LocalStorage (default, zero-dependency, filesystem) and
|
|
@@ -97,7 +124,8 @@ export class LocalStorage implements StorageBackend {
|
|
|
97
124
|
}
|
|
98
125
|
|
|
99
126
|
/**
|
|
100
|
-
* S3-compatible store (AWS S3, MinIO, ...). Opt-in;
|
|
127
|
+
* S3-compatible store (AWS S3, MinIO, ...). Opt-in; the app installs the SDK:
|
|
128
|
+
* `npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner`.
|
|
101
129
|
* Presigned GET URLs let clients fetch large blobs straight from object
|
|
102
130
|
* storage instead of streaming through the app.
|
|
103
131
|
*/
|
|
@@ -108,9 +136,12 @@ export class S3Storage implements StorageBackend {
|
|
|
108
136
|
constructor(opts: { endpoint?: string; key?: string; secret?: string; bucket?: string; region?: string } = {}) {
|
|
109
137
|
this.bucket = opts.bucket || process.env.TINA4_STORAGE_BUCKET || "";
|
|
110
138
|
if (!this.bucket) throw new Error("S3Storage requires TINA4_STORAGE_BUCKET");
|
|
111
|
-
// Loaded lazily; a missing
|
|
112
|
-
//
|
|
113
|
-
const { S3Client } =
|
|
139
|
+
// Loaded lazily; a missing SDK throws the install command here, and
|
|
140
|
+
// selectStorage() logs it and falls back to local storage.
|
|
141
|
+
const { S3Client } = loadAwsSdk("@aws-sdk/client-s3");
|
|
142
|
+
// url() needs the presigner; check it now so a half-installed SDK refuses
|
|
143
|
+
// at construction instead of failing on the first download link.
|
|
144
|
+
loadAwsSdk("@aws-sdk/s3-request-presigner");
|
|
114
145
|
const endpoint = opts.endpoint || process.env.TINA4_STORAGE_URL;
|
|
115
146
|
this.client = new S3Client({
|
|
116
147
|
endpoint: endpoint || undefined,
|
|
@@ -124,13 +155,13 @@ export class S3Storage implements StorageBackend {
|
|
|
124
155
|
}
|
|
125
156
|
|
|
126
157
|
async put(key: string, data: Buffer | string, mime = "application/octet-stream"): Promise<void> {
|
|
127
|
-
const { PutObjectCommand } =
|
|
158
|
+
const { PutObjectCommand } = loadAwsSdk("@aws-sdk/client-s3");
|
|
128
159
|
await this.client.send(new PutObjectCommand({ Bucket: this.bucket, Key: key, Body: data, ContentType: mime }));
|
|
129
160
|
}
|
|
130
161
|
|
|
131
162
|
async get(key: string): Promise<Buffer | null> {
|
|
132
163
|
try {
|
|
133
|
-
const { GetObjectCommand } =
|
|
164
|
+
const { GetObjectCommand } = loadAwsSdk("@aws-sdk/client-s3");
|
|
134
165
|
const res = await this.client.send(new GetObjectCommand({ Bucket: this.bucket, Key: key }));
|
|
135
166
|
const chunks: Buffer[] = [];
|
|
136
167
|
for await (const chunk of res.Body as AsyncIterable<Buffer>) chunks.push(Buffer.from(chunk));
|
|
@@ -141,14 +172,14 @@ export class S3Storage implements StorageBackend {
|
|
|
141
172
|
}
|
|
142
173
|
|
|
143
174
|
async url(key: string, ttl = 3600): Promise<string | null> {
|
|
144
|
-
const { GetObjectCommand } =
|
|
145
|
-
const { getSignedUrl } =
|
|
175
|
+
const { GetObjectCommand } = loadAwsSdk("@aws-sdk/client-s3");
|
|
176
|
+
const { getSignedUrl } = loadAwsSdk("@aws-sdk/s3-request-presigner");
|
|
146
177
|
return getSignedUrl(this.client, new GetObjectCommand({ Bucket: this.bucket, Key: key }), { expiresIn: ttl });
|
|
147
178
|
}
|
|
148
179
|
|
|
149
180
|
async delete(key: string): Promise<void> {
|
|
150
181
|
try {
|
|
151
|
-
const { DeleteObjectCommand } =
|
|
182
|
+
const { DeleteObjectCommand } = loadAwsSdk("@aws-sdk/client-s3");
|
|
152
183
|
await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
|
|
153
184
|
} catch (e) {
|
|
154
185
|
Log.error(`S3Storage delete failed for ${key}: ${(e as Error).message}`);
|
|
@@ -157,7 +188,7 @@ export class S3Storage implements StorageBackend {
|
|
|
157
188
|
|
|
158
189
|
async exists(key: string): Promise<boolean> {
|
|
159
190
|
try {
|
|
160
|
-
const { HeadObjectCommand } =
|
|
191
|
+
const { HeadObjectCommand } = loadAwsSdk("@aws-sdk/client-s3");
|
|
161
192
|
await this.client.send(new HeadObjectCommand({ Bucket: this.bucket, Key: key }));
|
|
162
193
|
return true;
|
|
163
194
|
} catch {
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
// Tina4 Seeder — seed database tables and ORM models with fake data.
|
|
2
10
|
// Zero external dependencies.
|
|
3
11
|
//
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
import { FakeData } from "./fakeData.js";
|
|
2
10
|
import { adapterExecute, adapterColumns, adapterInsert } from "./database.js";
|
|
3
11
|
import { Log } from "../../core/src/index.js";
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
/** Result of a seed run. */
|
|
2
10
|
export interface SeedSummary {
|
|
3
11
|
seeded: number;
|