tina4-nodejs 3.13.137 → 3.13.138

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.
Files changed (242) hide show
  1. package/CLAUDE.md +33 -3
  2. package/COMMERCIAL-LICENSE.md +8 -0
  3. package/LICENSE +373 -0
  4. package/NOTICE +11 -0
  5. package/README.md +24 -2
  6. package/package.json +44 -6
  7. package/packages/cli/dist/bin.js +19466 -18048
  8. package/packages/cli/src/bin.ts +8 -0
  9. package/packages/cli/src/commands/build.ts +8 -0
  10. package/packages/cli/src/commands/generate.ts +8 -0
  11. package/packages/cli/src/commands/init.ts +8 -0
  12. package/packages/cli/src/commands/lint.ts +8 -0
  13. package/packages/cli/src/commands/migrate.ts +8 -0
  14. package/packages/cli/src/commands/migrateCreate.ts +8 -0
  15. package/packages/cli/src/commands/migrateRollback.ts +8 -0
  16. package/packages/cli/src/commands/migrateStatus.ts +8 -0
  17. package/packages/cli/src/commands/queue.ts +8 -0
  18. package/packages/cli/src/commands/routes.ts +8 -0
  19. package/packages/cli/src/commands/seed.ts +8 -0
  20. package/packages/cli/src/commands/serve.ts +11 -0
  21. package/packages/cli/src/commands/test.ts +8 -0
  22. package/packages/core/dist/index.js +19458 -18043
  23. package/packages/core/gallery/auth/src/routes/api/gallery/auth/login/post.ts +8 -0
  24. package/packages/core/gallery/auth/src/routes/api/gallery/auth/verify/get.ts +8 -0
  25. package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +8 -0
  26. package/packages/core/gallery/database/src/routes/api/gallery/db/notes/get.ts +8 -0
  27. package/packages/core/gallery/database/src/routes/api/gallery/db/notes/post.ts +8 -0
  28. package/packages/core/gallery/database/src/routes/api/gallery/db/tables/get.ts +8 -0
  29. package/packages/core/gallery/error-overlay/src/routes/api/gallery/crash/get.ts +8 -0
  30. package/packages/core/gallery/orm/src/routes/api/gallery/products/get.ts +8 -0
  31. package/packages/core/gallery/orm/src/routes/api/gallery/products/post.ts +8 -0
  32. package/packages/core/gallery/queue/src/lib/queueDb.ts +8 -0
  33. package/packages/core/gallery/queue/src/routes/api/gallery/queue/consume/post.ts +8 -0
  34. package/packages/core/gallery/queue/src/routes/api/gallery/queue/fail/post.ts +8 -0
  35. package/packages/core/gallery/queue/src/routes/api/gallery/queue/produce/post.ts +8 -0
  36. package/packages/core/gallery/queue/src/routes/api/gallery/queue/retry/post.ts +8 -0
  37. package/packages/core/gallery/queue/src/routes/api/gallery/queue/status/get.ts +8 -0
  38. package/packages/core/gallery/queue/src/routes/gallery/queue/get.ts +8 -0
  39. package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/get.ts +8 -0
  40. package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/post.ts +8 -0
  41. package/packages/core/gallery/templates/src/routes/gallery/page/get.ts +8 -0
  42. package/packages/core/gallery/templates/src/templates/gallery_page.twig +8 -0
  43. package/packages/core/public/THIRD-PARTY-NOTICES.md +55 -0
  44. package/packages/core/public/bundled-component-provenance.json +48 -0
  45. package/packages/core/public/js/tina4js.min.js +13 -3
  46. package/packages/core/public/licenses/swagger-ui-3.10.0-original-notice.txt +11 -0
  47. package/packages/core/public/licenses/swagger-ui-5.17.14-NOTICE.txt +2 -0
  48. package/packages/core/public/licenses/swagger-ui-Apache-2.0.txt +202 -0
  49. package/packages/core/public/licenses/tina4-css-MIT.txt +21 -0
  50. package/packages/core/public/licenses/tina4-dev-admin-components.json +240 -0
  51. package/packages/core/public/licenses/tina4-dev-admin-third-party.txt +919 -0
  52. package/packages/core/public/licenses/tina4-js-MIT.txt +20 -0
  53. package/packages/core/src/__feedback/widget.js +8 -0
  54. package/packages/core/src/_missing.ts +8 -0
  55. package/packages/core/src/ai.ts +8 -0
  56. package/packages/core/src/aiClient.ts +8 -0
  57. package/packages/core/src/api.ts +15 -3
  58. package/packages/core/src/auth.ts +21 -8
  59. package/packages/core/src/authGate.ts +8 -0
  60. package/packages/core/src/background.ts +8 -0
  61. package/packages/core/src/cache.ts +43 -4
  62. package/packages/core/src/constants.ts +8 -0
  63. package/packages/core/src/container.ts +8 -0
  64. package/packages/core/src/context/chunker.ts +8 -0
  65. package/packages/core/src/context/index.ts +8 -0
  66. package/packages/core/src/devAdmin.ts +185 -59
  67. package/packages/core/src/devMailbox.ts +8 -0
  68. package/packages/core/src/dispatchPipeline.ts +8 -0
  69. package/packages/core/src/docs.ts +8 -0
  70. package/packages/core/src/docsAutoDiscovery.ts +8 -0
  71. package/packages/core/src/docsParser.ts +8 -0
  72. package/packages/core/src/docsScanner.ts +44 -0
  73. package/packages/core/src/docsSignatures.ts +8 -0
  74. package/packages/core/src/dotenv.ts +8 -0
  75. package/packages/core/src/env.ts +8 -0
  76. package/packages/core/src/errorOverlay.ts +8 -0
  77. package/packages/core/src/events.ts +8 -0
  78. package/packages/core/src/fakeData.ts +8 -0
  79. package/packages/core/src/feedback.ts +8 -0
  80. package/packages/core/src/graphql.ts +16 -5
  81. package/packages/core/src/health.ts +8 -0
  82. package/packages/core/src/htmlElement.ts +8 -0
  83. package/packages/core/src/i18n.ts +8 -0
  84. package/packages/core/src/index.ts +8 -0
  85. package/packages/core/src/job.ts +8 -0
  86. package/packages/core/src/logger.ts +8 -0
  87. package/packages/core/src/mcp.test.ts +8 -0
  88. package/packages/core/src/mcp.ts +8 -0
  89. package/packages/core/src/messenger.ts +143 -86
  90. package/packages/core/src/metrics.ts +13 -0
  91. package/packages/core/src/middleware.ts +1178 -1096
  92. package/packages/core/src/mqtt.ts +8 -0
  93. package/packages/core/src/mqttMessage.ts +8 -0
  94. package/packages/core/src/optionalPackage.ts +48 -0
  95. package/packages/core/src/plan.ts +8 -0
  96. package/packages/core/src/portTakeover.ts +8 -0
  97. package/packages/core/src/projectIndex.ts +8 -0
  98. package/packages/core/src/projectIndexExtractors.ts +8 -0
  99. package/packages/core/src/projectIndexStorage.ts +8 -0
  100. package/packages/core/src/push.ts +8 -0
  101. package/packages/core/src/queue.ts +8 -0
  102. package/packages/core/src/queueBackends/kafkaBackend.ts +8 -0
  103. package/packages/core/src/queueBackends/liteBackend.ts +8 -0
  104. package/packages/core/src/queueBackends/mongoBackend.ts +21 -8
  105. package/packages/core/src/queueBackends/rabbitmqBackend.ts +8 -0
  106. package/packages/core/src/rateLimiter.ts +8 -0
  107. package/packages/core/src/request.ts +59 -10
  108. package/packages/core/src/response.ts +79 -5
  109. package/packages/core/src/routeDiscovery.ts +8 -0
  110. package/packages/core/src/router.ts +8 -0
  111. package/packages/core/src/server.ts +73 -24
  112. package/packages/core/src/service.ts +8 -0
  113. package/packages/core/src/session.ts +8 -0
  114. package/packages/core/src/sessionHandlers/childError.ts +8 -0
  115. package/packages/core/src/sessionHandlers/databaseHandler.ts +8 -0
  116. package/packages/core/src/sessionHandlers/memcachedHandler.ts +8 -0
  117. package/packages/core/src/sessionHandlers/mongoClient.ts +8 -0
  118. package/packages/core/src/sessionHandlers/mongoHandler.ts +8 -0
  119. package/packages/core/src/sessionHandlers/respClient.ts +8 -0
  120. package/packages/core/src/sessionHandlers/sqlClient.ts +8 -0
  121. package/packages/core/src/sessionHandlers/syncBridge.ts +8 -0
  122. package/packages/core/src/sessionHandlers/syncSocket.ts +8 -0
  123. package/packages/core/src/sessionHandlers/valkeyHandler.ts +8 -0
  124. package/packages/core/src/sso.ts +8 -0
  125. package/packages/core/src/static.ts +8 -0
  126. package/packages/core/src/test.ts +8 -0
  127. package/packages/core/src/testClient.ts +8 -0
  128. package/packages/core/src/testing.ts +8 -0
  129. package/packages/core/src/transport.ts +369 -0
  130. package/packages/core/src/trustedProxy.ts +8 -0
  131. package/packages/core/src/types.ts +13 -0
  132. package/packages/core/src/validator.ts +8 -0
  133. package/packages/core/src/version.ts +8 -0
  134. package/packages/core/src/websocket.ts +8 -0
  135. package/packages/core/src/websocketBackplane.ts +49 -32
  136. package/packages/core/src/websocketConnection.ts +8 -0
  137. package/packages/core/src/wsdl.ts +197 -115
  138. package/packages/core/templates/errors/302.twig +8 -0
  139. package/packages/core/templates/errors/401.twig +8 -0
  140. package/packages/core/templates/errors/403.twig +8 -0
  141. package/packages/core/templates/errors/404.twig +8 -0
  142. package/packages/core/templates/errors/500.twig +8 -0
  143. package/packages/core/templates/errors/502.twig +8 -0
  144. package/packages/core/templates/errors/503.twig +8 -0
  145. package/packages/core/templates/errors/base.twig +8 -0
  146. package/packages/frond/dist/index.js +151 -17
  147. package/packages/frond/src/engine.ts +177 -17
  148. package/packages/frond/src/index.ts +8 -0
  149. package/packages/orm/dist/index.js +8125 -6708
  150. package/packages/orm/src/adapters/firebird.ts +50 -3
  151. package/packages/orm/src/adapters/mongodb.ts +8 -0
  152. package/packages/orm/src/adapters/mssql.ts +29 -7
  153. package/packages/orm/src/adapters/mysql.ts +17 -1
  154. package/packages/orm/src/adapters/odbc.ts +11 -0
  155. package/packages/orm/src/adapters/pg-types.d.ts +8 -0
  156. package/packages/orm/src/adapters/postgres.ts +34 -8
  157. package/packages/orm/src/adapters/sqlDialect.ts +57 -0
  158. package/packages/orm/src/adapters/sqlite.ts +26 -0
  159. package/packages/orm/src/autoCrud.ts +78 -36
  160. package/packages/orm/src/baseModel.ts +61 -29
  161. package/packages/orm/src/cachedDatabase.ts +67 -1
  162. package/packages/orm/src/connectTimeout.ts +8 -0
  163. package/packages/orm/src/database.ts +649 -401
  164. package/packages/orm/src/databaseResult.ts +8 -0
  165. package/packages/orm/src/databaseUrl.ts +8 -0
  166. package/packages/orm/src/docstore.ts +23 -1
  167. package/packages/orm/src/fakeData.ts +8 -0
  168. package/packages/orm/src/graph/adapters/arango.ts +8 -0
  169. package/packages/orm/src/graph/adapters/bolt.ts +8 -0
  170. package/packages/orm/src/graph/adapters/ultipa.ts +8 -0
  171. package/packages/orm/src/graph/connectTimeout.ts +8 -0
  172. package/packages/orm/src/graph/errors.ts +8 -0
  173. package/packages/orm/src/graph/graphAdapter.ts +8 -0
  174. package/packages/orm/src/graph/graphDatabase.ts +8 -0
  175. package/packages/orm/src/graph/graphUrl.ts +10 -1
  176. package/packages/orm/src/graph/shapes.ts +8 -0
  177. package/packages/orm/src/index.ts +147 -139
  178. package/packages/orm/src/migration.ts +8 -0
  179. package/packages/orm/src/model.ts +8 -0
  180. package/packages/orm/src/modelCollection.ts +8 -0
  181. package/packages/orm/src/point.ts +8 -0
  182. package/packages/orm/src/query.ts +149 -33
  183. package/packages/orm/src/queryBuilder.ts +8 -0
  184. package/packages/orm/src/realtime/index.ts +8 -0
  185. package/packages/orm/src/realtime/models/attachment.ts +8 -0
  186. package/packages/orm/src/realtime/models/channel.ts +8 -0
  187. package/packages/orm/src/realtime/models/channelMember.ts +8 -0
  188. package/packages/orm/src/realtime/models/message.ts +8 -0
  189. package/packages/orm/src/realtime/models/workspace.ts +8 -0
  190. package/packages/orm/src/realtime/realtime.ts +8 -0
  191. package/packages/orm/src/realtime/storage.ts +43 -12
  192. package/packages/orm/src/seeder.ts +8 -0
  193. package/packages/orm/src/seederTable.ts +8 -0
  194. package/packages/orm/src/seederTypes.ts +8 -0
  195. package/packages/orm/src/sqlTranslator.ts +185 -8
  196. package/packages/orm/src/types.ts +15 -0
  197. package/packages/orm/src/validation.ts +8 -0
  198. package/packages/swagger/src/generator.ts +8 -0
  199. package/packages/swagger/src/index.ts +8 -0
  200. package/packages/swagger/src/ui.ts +8 -0
  201. package/types/core/src/background.d.ts +0 -16
  202. package/types/core/src/cache.d.ts +6 -0
  203. package/types/core/src/devAdmin.d.ts +18 -12
  204. package/types/core/src/docs.d.ts +0 -17
  205. package/types/core/src/errorOverlay.d.ts +0 -24
  206. package/types/core/src/events.d.ts +0 -12
  207. package/types/core/src/feedback.d.ts +0 -23
  208. package/types/core/src/graphql.d.ts +0 -21
  209. package/types/core/src/htmlElement.d.ts +0 -15
  210. package/types/core/src/messenger.d.ts +12 -1
  211. package/types/core/src/metrics.d.ts +2 -0
  212. package/types/core/src/middleware.d.ts +45 -1
  213. package/types/core/src/mqtt.d.ts +0 -33
  214. package/types/core/src/optionalPackage.d.ts +25 -0
  215. package/types/core/src/plan.d.ts +0 -10
  216. package/types/core/src/projectIndex.d.ts +0 -10
  217. package/types/core/src/queueBackends/mongoBackend.d.ts +2 -0
  218. package/types/core/src/request.d.ts +12 -0
  219. package/types/core/src/response.d.ts +13 -0
  220. package/types/core/src/server.d.ts +19 -0
  221. package/types/core/src/sessionHandlers/childError.d.ts +0 -15
  222. package/types/core/src/transport.d.ts +28 -0
  223. package/types/core/src/trustedProxy.d.ts +0 -16
  224. package/types/core/src/types.d.ts +5 -0
  225. package/types/core/src/websocketBackplane.d.ts +1 -0
  226. package/types/core/src/wsdl.d.ts +8 -22
  227. package/types/frond/src/engine.d.ts +7 -1
  228. package/types/orm/src/adapters/firebird.d.ts +12 -0
  229. package/types/orm/src/adapters/mssql.d.ts +11 -0
  230. package/types/orm/src/adapters/mysql.d.ts +2 -0
  231. package/types/orm/src/adapters/postgres.d.ts +11 -0
  232. package/types/orm/src/adapters/sqlDialect.d.ts +25 -0
  233. package/types/orm/src/adapters/sqlite.d.ts +7 -0
  234. package/types/orm/src/baseModel.d.ts +5 -0
  235. package/types/orm/src/cachedDatabase.d.ts +27 -0
  236. package/types/orm/src/database.d.ts +46 -7
  237. package/types/orm/src/graph/graphUrl.d.ts +0 -8
  238. package/types/orm/src/index.d.ts +1 -1
  239. package/types/orm/src/query.d.ts +48 -2
  240. package/types/orm/src/realtime/storage.d.ts +2 -1
  241. package/types/orm/src/sqlTranslator.d.ts +70 -18
  242. package/types/orm/src/types.d.ts +6 -0
@@ -1,5 +1,14 @@
1
- import type { QueryOptions } from "./types.js";
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
- ): { sql: string; countSql: string; params: unknown[]; limit: number; offset: number; page: number } {
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
- const sqlOp = operatorMap[op];
33
- if (sqlOp) {
34
- conditions.push(`"${field}" ${sqlOp} ?`);
35
- params.push(opVal);
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(`"${field}" = ?`);
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 = options.sort.split(",").map((s) => {
52
- const trimmed = s.trim();
53
- if (trimmed.startsWith("-")) {
54
- return `"${trimmed.slice(1)}" DESC`;
55
- }
56
- return `"${trimmed}" ASC`;
57
- });
58
- orderClause = `ORDER BY ${parts.join(", ")}`;
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 sql = `SELECT * FROM "${tableName}" ${whereClause} ${orderClause} LIMIT ? OFFSET ?`;
75
- const countSql = `SELECT COUNT(*) as total FROM "${tableName}" ${whereClause}`;
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
- const filter: Record<string, unknown> = {};
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
- const filterMatch = key.match(/^filter\[(\w+)\](?:\[(\w+)\])?$/);
94
- if (filterMatch) {
95
- const field = filterMatch[1];
96
- const operator = filterMatch[2];
97
- if (operator) {
98
- if (!filter[field] || typeof filter[field] !== "object") {
99
- filter[field] = {};
100
- }
101
- (filter[field] as Record<string, string>)[operator] = value;
102
- } else {
103
- filter[field] = value;
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. A missing driver throws here and
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; needs @aws-sdk/client-s3.
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 driver throws here and selectStorage falls back.
112
- // eslint-disable-next-line @typescript-eslint/no-require-imports
113
- const { S3Client } = require("@aws-sdk/client-s3");
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 } = require("@aws-sdk/client-s3");
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 } = require("@aws-sdk/client-s3");
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 } = require("@aws-sdk/client-s3");
145
- const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
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 } = require("@aws-sdk/client-s3");
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 } = require("@aws-sdk/client-s3");
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;