tina4-nodejs 3.13.92 → 3.13.95

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 (193) hide show
  1. package/CLAUDE.md +170 -28
  2. package/README.md +2 -2
  3. package/package.json +13 -9
  4. package/packages/cli/dist/bin.js +33126 -30055
  5. package/packages/cli/src/commands/metrics.ts +17 -11
  6. package/packages/cli/src/commands/serve.ts +10 -9
  7. package/packages/core/dist/index.js +33062 -29908
  8. package/packages/core/src/ai.ts +7 -1
  9. package/packages/core/src/auth.ts +191 -39
  10. package/packages/core/src/background.ts +19 -19
  11. package/packages/core/src/cache.ts +492 -49
  12. package/packages/core/src/devAdmin.ts +79 -32
  13. package/packages/core/src/devMailbox.ts +20 -44
  14. package/packages/core/src/dispatchPipeline.ts +285 -0
  15. package/packages/core/src/dotenv.ts +185 -40
  16. package/packages/core/src/index.ts +7 -6
  17. package/packages/core/src/logger.ts +257 -36
  18. package/packages/core/src/mcp.ts +1 -1
  19. package/packages/core/src/messenger.ts +81 -13
  20. package/packages/core/src/metrics.ts +199 -961
  21. package/packages/core/src/middleware.ts +390 -123
  22. package/packages/core/src/queue.ts +188 -32
  23. package/packages/core/src/queueBackends/kafkaBackend.ts +109 -13
  24. package/packages/core/src/queueBackends/liteBackend.ts +13 -0
  25. package/packages/core/src/queueBackends/mongoBackend.ts +101 -9
  26. package/packages/core/src/queueBackends/rabbitmqBackend.ts +22 -4
  27. package/packages/core/src/rateLimiter.ts +10 -5
  28. package/packages/core/src/request.ts +6 -9
  29. package/packages/core/src/response.ts +46 -1
  30. package/packages/core/src/router.ts +29 -4
  31. package/packages/core/src/server.ts +751 -414
  32. package/packages/core/src/session.ts +244 -27
  33. package/packages/core/src/sessionHandlers/childError.ts +72 -0
  34. package/packages/core/src/sessionHandlers/databaseHandler.ts +338 -48
  35. package/packages/core/src/sessionHandlers/memcachedHandler.ts +181 -0
  36. package/packages/core/src/sessionHandlers/mongoClient.ts +293 -202
  37. package/packages/core/src/sessionHandlers/mongoHandler.ts +88 -8
  38. package/packages/core/src/sessionHandlers/respClient.ts +16 -143
  39. package/packages/core/src/sessionHandlers/sqlClient.ts +290 -0
  40. package/packages/core/src/sessionHandlers/syncBridge.ts +190 -0
  41. package/packages/core/src/sessionHandlers/syncSocket.ts +236 -0
  42. package/packages/core/src/testClient.ts +18 -5
  43. package/packages/core/src/trustedProxy.ts +249 -0
  44. package/packages/core/src/types.ts +29 -5
  45. package/packages/core/src/websocket.ts +66 -0
  46. package/packages/frond/dist/index.js +74 -31
  47. package/packages/frond/src/engine.ts +99 -33
  48. package/packages/orm/dist/index.js +26554 -23400
  49. package/packages/orm/src/adapters/firebird.ts +183 -56
  50. package/packages/orm/src/adapters/mongodb.ts +25 -4
  51. package/packages/orm/src/adapters/mssql.ts +114 -29
  52. package/packages/orm/src/adapters/mysql.ts +103 -40
  53. package/packages/orm/src/adapters/odbc.ts +44 -21
  54. package/packages/orm/src/adapters/postgres.ts +118 -26
  55. package/packages/orm/src/adapters/sqlDialect.ts +120 -0
  56. package/packages/orm/src/adapters/sqlite.ts +64 -25
  57. package/packages/orm/src/baseModel.ts +135 -40
  58. package/packages/orm/src/cachedDatabase.ts +43 -19
  59. package/packages/orm/src/connectTimeout.ts +265 -0
  60. package/packages/orm/src/database.ts +338 -198
  61. package/packages/orm/src/databaseResult.ts +65 -13
  62. package/packages/orm/src/databaseUrl.ts +484 -0
  63. package/packages/orm/src/docstore.ts +386 -145
  64. package/packages/orm/src/index.ts +13 -3
  65. package/packages/orm/src/migration.ts +18 -3
  66. package/packages/orm/src/queryBuilder.ts +38 -4
  67. package/packages/orm/src/sqlTranslator.ts +310 -4
  68. package/packages/orm/src/types.ts +15 -4
  69. package/types/cli/src/bin.d.ts +92 -0
  70. package/types/cli/src/commands/build.d.ts +2 -0
  71. package/types/cli/src/commands/generate.d.ts +47 -0
  72. package/types/cli/src/commands/init.d.ts +1 -0
  73. package/types/cli/src/commands/metrics.d.ts +6 -0
  74. package/types/cli/src/commands/migrate.d.ts +1 -0
  75. package/types/cli/src/commands/migrateCreate.d.ts +1 -0
  76. package/types/cli/src/commands/migrateRollback.d.ts +1 -0
  77. package/types/cli/src/commands/migrateStatus.d.ts +1 -0
  78. package/types/cli/src/commands/queue.d.ts +20 -0
  79. package/types/cli/src/commands/routes.d.ts +1 -0
  80. package/types/cli/src/commands/seed.d.ts +1 -0
  81. package/types/cli/src/commands/serve.d.ts +6 -0
  82. package/types/cli/src/commands/test.d.ts +1 -0
  83. package/types/core/src/ai.d.ts +64 -0
  84. package/types/core/src/api.d.ts +262 -0
  85. package/types/core/src/auth.d.ts +177 -0
  86. package/types/core/src/authGate.d.ts +20 -0
  87. package/types/core/src/background.d.ts +34 -0
  88. package/types/core/src/cache.d.ts +163 -0
  89. package/types/core/src/constants.d.ts +38 -0
  90. package/types/core/src/container.d.ts +44 -0
  91. package/types/core/src/context/chunker.d.ts +31 -0
  92. package/types/core/src/context/index.d.ts +93 -0
  93. package/types/core/src/devAdmin.d.ts +179 -0
  94. package/types/core/src/devMailbox.d.ts +54 -0
  95. package/types/core/src/dispatchPipeline.d.ts +117 -0
  96. package/types/core/src/docs.d.ts +141 -0
  97. package/types/core/src/docsAutoDiscovery.d.ts +6 -0
  98. package/types/core/src/dotenv.d.ts +87 -0
  99. package/types/core/src/env.d.ts +28 -0
  100. package/types/core/src/errorOverlay.d.ts +36 -0
  101. package/types/core/src/events.d.ts +75 -0
  102. package/types/core/src/fakeData.d.ts +55 -0
  103. package/types/core/src/feedback.d.ts +90 -0
  104. package/types/core/src/graphql.d.ts +207 -0
  105. package/types/core/src/health.d.ts +22 -0
  106. package/types/core/src/htmlElement.d.ts +75 -0
  107. package/types/core/src/i18n.d.ts +37 -0
  108. package/types/core/src/index.d.ts +92 -0
  109. package/types/core/src/job.d.ts +39 -0
  110. package/types/core/src/logger.d.ts +200 -0
  111. package/types/core/src/mcp.d.ts +248 -0
  112. package/types/core/src/messenger.d.ts +191 -0
  113. package/types/core/src/metrics.d.ts +41 -0
  114. package/types/core/src/middleware.d.ts +330 -0
  115. package/types/core/src/mqtt.d.ts +257 -0
  116. package/types/core/src/mqttMessage.d.ts +67 -0
  117. package/types/core/src/plan.d.ts +96 -0
  118. package/types/core/src/projectIndex.d.ts +56 -0
  119. package/types/core/src/queue.d.ts +268 -0
  120. package/types/core/src/queueBackends/kafkaBackend.d.ts +117 -0
  121. package/types/core/src/queueBackends/liteBackend.d.ts +128 -0
  122. package/types/core/src/queueBackends/mongoBackend.d.ts +119 -0
  123. package/types/core/src/queueBackends/rabbitmqBackend.d.ts +55 -0
  124. package/types/core/src/rateLimiter.d.ts +49 -0
  125. package/types/core/src/request.d.ts +25 -0
  126. package/types/core/src/response.d.ts +28 -0
  127. package/types/core/src/routeDiscovery.d.ts +12 -0
  128. package/types/core/src/router.d.ts +366 -0
  129. package/types/core/src/scss.d.ts +19 -0
  130. package/types/core/src/server.d.ts +146 -0
  131. package/types/core/src/service.d.ts +115 -0
  132. package/types/core/src/session.d.ts +341 -0
  133. package/types/core/src/sessionHandlers/childError.d.ts +34 -0
  134. package/types/core/src/sessionHandlers/databaseHandler.d.ts +97 -0
  135. package/types/core/src/sessionHandlers/memcachedHandler.d.ts +60 -0
  136. package/types/core/src/sessionHandlers/mongoClient.d.ts +35 -0
  137. package/types/core/src/sessionHandlers/mongoHandler.d.ts +109 -0
  138. package/types/core/src/sessionHandlers/respClient.d.ts +22 -0
  139. package/types/core/src/sessionHandlers/sqlClient.d.ts +39 -0
  140. package/types/core/src/sessionHandlers/syncBridge.d.ts +91 -0
  141. package/types/core/src/sessionHandlers/syncSocket.d.ts +49 -0
  142. package/types/core/src/sessionHandlers/valkeyHandler.d.ts +65 -0
  143. package/types/core/src/static.d.ts +2 -0
  144. package/types/core/src/test.d.ts +94 -0
  145. package/types/core/src/testClient.d.ts +36 -0
  146. package/types/core/src/testing.d.ts +58 -0
  147. package/types/core/src/trustedProxy.d.ts +44 -0
  148. package/types/core/src/types.d.ts +242 -0
  149. package/types/core/src/validator.d.ts +52 -0
  150. package/types/core/src/websocket.d.ts +402 -0
  151. package/types/core/src/websocketBackplane.d.ts +166 -0
  152. package/types/core/src/websocketConnection.d.ts +54 -0
  153. package/types/core/src/wsdl.d.ts +101 -0
  154. package/types/frond/src/engine.d.ts +263 -0
  155. package/types/frond/src/index.d.ts +2 -0
  156. package/types/orm/src/adapters/firebird.d.ts +183 -0
  157. package/types/orm/src/adapters/mongodb.d.ts +81 -0
  158. package/types/orm/src/adapters/mssql.d.ts +77 -0
  159. package/types/orm/src/adapters/mysql.d.ts +67 -0
  160. package/types/orm/src/adapters/odbc.d.ts +94 -0
  161. package/types/orm/src/adapters/postgres.d.ts +86 -0
  162. package/types/orm/src/adapters/sqlDialect.d.ts +71 -0
  163. package/types/orm/src/adapters/sqlite.d.ts +68 -0
  164. package/types/orm/src/autoCrud.d.ts +73 -0
  165. package/types/orm/src/baseModel.d.ts +427 -0
  166. package/types/orm/src/cachedDatabase.d.ts +190 -0
  167. package/types/orm/src/connectTimeout.d.ts +100 -0
  168. package/types/orm/src/database.d.ts +655 -0
  169. package/types/orm/src/databaseResult.d.ts +109 -0
  170. package/types/orm/src/databaseUrl.d.ts +125 -0
  171. package/types/orm/src/docstore.d.ts +241 -0
  172. package/types/orm/src/fakeData.d.ts +22 -0
  173. package/types/orm/src/index.d.ts +43 -0
  174. package/types/orm/src/migration.d.ts +275 -0
  175. package/types/orm/src/model.d.ts +7 -0
  176. package/types/orm/src/query.d.ts +14 -0
  177. package/types/orm/src/queryBuilder.d.ts +193 -0
  178. package/types/orm/src/realtime/index.d.ts +7 -0
  179. package/types/orm/src/realtime/models/attachment.d.ts +43 -0
  180. package/types/orm/src/realtime/models/channel.d.ts +32 -0
  181. package/types/orm/src/realtime/models/channelMember.d.ts +32 -0
  182. package/types/orm/src/realtime/models/message.d.ts +36 -0
  183. package/types/orm/src/realtime/models/workspace.d.ts +26 -0
  184. package/types/orm/src/realtime/realtime.d.ts +24 -0
  185. package/types/orm/src/realtime/storage.d.ts +61 -0
  186. package/types/orm/src/seeder.d.ts +118 -0
  187. package/types/orm/src/sqlTranslator.d.ts +258 -0
  188. package/types/orm/src/types.d.ts +148 -0
  189. package/types/orm/src/validation.d.ts +6 -0
  190. package/types/swagger/src/generator.d.ts +46 -0
  191. package/types/swagger/src/index.d.ts +2 -0
  192. package/types/swagger/src/ui.d.ts +11 -0
  193. package/packages/core/src/sessionHandlers/redisHandler.ts +0 -206
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Tina4 WSDL/SOAP — SOAP 1.1 / WSDL 1.1 service base class.
3
+ *
4
+ * Auto-generates WSDL definitions and handles SOAP XML requests.
5
+ * Zero external dependencies — uses simple string parsing for XML.
6
+ *
7
+ * Matches the PHP reference implementation (Tina4\WSDL).
8
+ *
9
+ * import { WSDLService, WSDLOperation } from "@tina4/core";
10
+ *
11
+ * class Calculator extends WSDLService {
12
+ * serviceName = "Calculator";
13
+ * serviceUrl = "/api/calculator";
14
+ *
15
+ * @WSDLOperation({ output: { Result: "int" } })
16
+ * async Add(a: number, b: number): Promise<Record<string, unknown>> {
17
+ * return { Result: a + b };
18
+ * }
19
+ * }
20
+ */
21
+ export interface WSDLOperationMeta {
22
+ name: string;
23
+ description?: string;
24
+ input?: Record<string, string>;
25
+ output?: Record<string, string>;
26
+ }
27
+ interface WSDLOperationConfig {
28
+ description?: string;
29
+ input?: Record<string, string>;
30
+ output?: Record<string, string>;
31
+ }
32
+ /**
33
+ * Decorator function for marking methods as WSDL operations.
34
+ *
35
+ * @WSDLOperation({ description: "Add two numbers", input: { a: "int", b: "int" }, output: { Result: "int" } })
36
+ * async Add(a: number, b: number): Promise<Record<string, unknown>> { ... }
37
+ */
38
+ export declare function WSDLOperation(config?: WSDLOperationConfig): (_target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
39
+ export declare abstract class WSDLService {
40
+ abstract serviceName: string;
41
+ abstract serviceUrl: string;
42
+ protected namespace: string;
43
+ /**
44
+ * Lifecycle hook: called before operation invocation.
45
+ * Override to validate, log, or modify the incoming request.
46
+ */
47
+ protected onRequest(_request: unknown): void;
48
+ /**
49
+ * Lifecycle hook: called after operation returns.
50
+ * Override to transform, audit, or enrich the result.
51
+ * Must return the (possibly modified) result.
52
+ */
53
+ protected onResult(result: Record<string, unknown>): Record<string, unknown>;
54
+ /** Discovered operations (populated on first use). */
55
+ private _operations;
56
+ /**
57
+ * Discover operations by scanning for methods with _wsdlOp metadata.
58
+ */
59
+ private discoverOperations;
60
+ /**
61
+ * Map a type name to an XSD type string.
62
+ */
63
+ private typeToXsd;
64
+ /**
65
+ * Convert a string value from XML to the target type.
66
+ */
67
+ private convertValue;
68
+ /**
69
+ * Generate WSDL 1.1 XML document.
70
+ */
71
+ generateWSDL(endpointUrl?: string): string;
72
+ /**
73
+ * Handle incoming SOAP request (parse XML, dispatch to method, return SOAP response).
74
+ */
75
+ handle(soapXml?: string): Promise<string>;
76
+ /**
77
+ * Register this service's routes on a router.
78
+ * GET /service-url?wsdl -> WSDL XML
79
+ * POST /service-url -> Handle SOAP request
80
+ */
81
+ register(router: {
82
+ addRoute?: (method: string, path: string, handler: (req: unknown, res: unknown) => void) => void;
83
+ }): void;
84
+ /**
85
+ * Handle GET request — return WSDL XML.
86
+ */
87
+ private handleGetRequest;
88
+ /**
89
+ * Handle POST request — process SOAP XML.
90
+ */
91
+ private handlePostRequest;
92
+ /**
93
+ * Build a SOAP response XML envelope.
94
+ */
95
+ private soapResponse;
96
+ /**
97
+ * Build a SOAP fault response XML.
98
+ */
99
+ private soapFault;
100
+ }
101
+ export {};
@@ -0,0 +1,263 @@
1
+ export type FilterFn = (value: unknown, ...args: unknown[]) => unknown;
2
+ export type TestFn = (value: unknown) => boolean;
3
+ /** A minimal request shape a {% live %} data provider receives. */
4
+ export interface LiveRequest {
5
+ headers?: Record<string, unknown>;
6
+ params?: Record<string, string>;
7
+ }
8
+ /** A {% live %} data provider — re-runs with the live request each refresh. */
9
+ export type LiveProvider = (req: LiveRequest) => Record<string, unknown>;
10
+ /** Result of respondLive — a pure {status, body} descriptor a route applies. */
11
+ export interface LiveResponse {
12
+ status: number;
13
+ body: string;
14
+ }
15
+ /** WebSocket broadcaster hook wired by @tina4/core so pushLive can broadcast. */
16
+ export type LiveBroadcaster = (wsPath: string | null, name: string, envelope: string) => void;
17
+ /**
18
+ * Hard cap on the template caches — `compiled` and `compiledStrings`
19
+ * (ADR-0004, parity with PHP/Python/Ruby TEMPLATE_CACHE_MAX).
20
+ *
21
+ * An entry here is a whole token list, so the cap sits well below what a
22
+ * per-expression memo would justify. 256 is far above any real application's
23
+ * template count, so a normal app never evicts. The cap exists for the
24
+ * workload that genuinely grows without limit for the life of a worker:
25
+ * `renderString` keys on md5(source), so an app that builds template strings
26
+ * dynamically adds an entry per distinct string.
27
+ */
28
+ export declare const TEMPLATE_CACHE_MAX = 256;
29
+ /**
30
+ * Set the session ID used by formToken() / form_token() for CSRF session binding.
31
+ */
32
+ export declare function setFormTokenSessionId(sessionId: string): void;
33
+ export declare class Frond {
34
+ private static classFilters;
35
+ private static classGlobals;
36
+ private static classTests;
37
+ private static liveFragments;
38
+ private static liveSources;
39
+ private static liveWsPaths;
40
+ private static liveBroadcaster;
41
+ /**
42
+ * Register a custom filter at the class level — available to every
43
+ * future ``new Frond()`` instance. Callable as ``Frond.addFilter()``
44
+ * (static) or ``frond.addFilter()`` (instance). See instance method
45
+ * below for the dual-call semantics.
46
+ */
47
+ static addFilter(name: string, fn: FilterFn): void;
48
+ /**
49
+ * Register a global variable available in all templates of every
50
+ * future instance. Callable as ``Frond.addGlobal()`` (static) or
51
+ * ``frond.addGlobal()`` (instance).
52
+ */
53
+ static addGlobal(name: string, value: unknown): void;
54
+ /**
55
+ * Register a custom test (``{% if x is positive %}``) at the class
56
+ * level. Callable as ``Frond.addTest()`` (static) or
57
+ * ``frond.addTest()`` (instance).
58
+ */
59
+ static addTest(name: string, fn: TestFn): void;
60
+ /**
61
+ * Clear the class-level globals/filters/tests registries.
62
+ * Useful in test fixtures to prevent leaking state between tests.
63
+ * Does NOT affect built-in filters or globals — only user-registered
64
+ * ones via Frond.addFilter / addGlobal / addTest.
65
+ */
66
+ static clearRegistry(): void;
67
+ private templateDir;
68
+ private filters;
69
+ private globals;
70
+ private tests;
71
+ private _sandbox;
72
+ private _allowedFilters;
73
+ private _allowedTags;
74
+ private _allowedVars;
75
+ private fragmentCache;
76
+ private _autoEscape;
77
+ /**
78
+ * Token pre-compilation cache for file templates.
79
+ *
80
+ * `cachedAt` is captured so the TINA4_TEMPLATE_CACHE_TTL env var can
81
+ * force re-compilation after N seconds even in production. TTL of 0
82
+ * means "no time-based invalidation" — entries live forever.
83
+ */
84
+ private compiled;
85
+ /** Token pre-compilation cache for string templates */
86
+ private compiledStrings;
87
+ /**
88
+ * Bound reference to `applyFilters`, stashed into the render context as
89
+ * `__frond_apply_filters__` so the module-level `evalExpr` can resolve a
90
+ * filter pipe using THIS instance's registered filters. Bound once. (#171)
91
+ */
92
+ private readonly _applyFiltersBound;
93
+ getTemplateDir(): string;
94
+ constructor(templateDir?: string);
95
+ sandbox(filters?: string[], tags?: string[], vars?: string[]): Frond;
96
+ unsandbox(): Frond;
97
+ /**
98
+ * Register a custom filter. The filter is persisted at class level
99
+ * so new instances created by hot-reload inherit it automatically;
100
+ * the live instance's local filter map also receives the addition
101
+ * immediately. Mirrors Python's _ClassOrInstanceMethod dual-call.
102
+ */
103
+ addFilter(name: string, fn: FilterFn): void;
104
+ /**
105
+ * Register a global variable available in all templates. Persisted
106
+ * at class level — see ``addFilter`` for the dual-call semantics.
107
+ */
108
+ addGlobal(name: string, value: unknown): void;
109
+ /**
110
+ * Register a custom test. Persisted at class level — see
111
+ * ``addFilter`` for the dual-call semantics.
112
+ */
113
+ addTest(name: string, fn: TestFn): void;
114
+ /**
115
+ * Read the cache TTL in seconds. `TINA4_TEMPLATE_CACHE_TTL=0` (the
116
+ * default) keeps the existing "cache forever in prod" behaviour — any
117
+ * positive value invalidates compiled tokens after N seconds, useful
118
+ * when running long-lived servers behind a slow file sync where mtime
119
+ * isn't a reliable freshness signal.
120
+ */
121
+ private cacheTtlSeconds;
122
+ render(template: string, data?: Record<string, unknown>): string;
123
+ renderString(source: string, data?: Record<string, unknown>): string;
124
+ /** Clear all compiled template caches. */
125
+ clearCache(): void;
126
+ /** Render a debug dump of a value as HTML — parity with PHP/Ruby/Python.
127
+ * Gated on TINA4_DEBUG=true. Returns empty string in production. */
128
+ renderDump(value: unknown): string;
129
+ private load;
130
+ /** Execute pre-tokenized template against context. */
131
+ private executeCached;
132
+ /** Execute with both source and pre-tokenized tokens available. */
133
+ private executeWithSource;
134
+ private execute;
135
+ private extractBlocks;
136
+ private renderWithBlocks;
137
+ private renderTokens;
138
+ /**
139
+ * May this filter RUN under the current sandbox?
140
+ *
141
+ * The escaping decision has to ask this rather than read the filter name out of
142
+ * the source. Node carries safety as a FLAG rather than as a value-level marker
143
+ * (Python and Ruby return a SafeString, PHP prepends a RAW_MARKER -- all three
144
+ * produced only by actually running the filter), so here the name alone was
145
+ * enough to suppress auto-escaping even when the filter was denied and skipped.
146
+ */
147
+ private filterPermitted;
148
+ /**
149
+ * May this tag run under the current sandbox?
150
+ *
151
+ * One gate for every tag, so the allow-list governs the whole tag vocabulary
152
+ * instead of the four names that happened to be checked individually.
153
+ */
154
+ private tagPermitted;
155
+ /**
156
+ * Consume a denied tag WITHOUT running it, returning the index past its body.
157
+ *
158
+ * Advancing a single token past a body-owning tag would leave the body's tokens
159
+ * to render at the TOP level, leaking exactly the content the sandbox denied.
160
+ */
161
+ private skipDeniedTag;
162
+ private skipBlock;
163
+ /**
164
+ * Apply a parsed filter chain to an already-evaluated value. This is the
165
+ * instance-aware filter engine used by `evalExpr` (via the
166
+ * `__frond_apply_filters__` hook in the render context) so filters resolve
167
+ * with this Frond's registered/custom filters at ANY nesting depth — inside
168
+ * concat operands, ternary branches, and parenthesised sub-expressions — not
169
+ * only at the top-level {{ }} output. Mirrors the filter loop in
170
+ * `evalVarRaw`: `first`/`last` tail-paths, registered `this.filters`, and the
171
+ * trailing-comparison form (`length != 1`). Auto-escaping stays the caller's
172
+ * concern (`evalVarInner`). (#171)
173
+ */
174
+ private applyFilters;
175
+ private evalVar;
176
+ private evalVarRaw;
177
+ private evalVarInner;
178
+ private handleIf;
179
+ private handleFor;
180
+ private handleSet;
181
+ private handleInclude;
182
+ private handleMacro;
183
+ /**
184
+ * Parse a macro parameter list into [name, default] pairs.
185
+ *
186
+ * Handles: name, name="default", name='default'. Splitting on "," alone left a
187
+ * defaulted parameter literally NAMED `greeting='Hello'`, so the body's
188
+ * {{ greeting }} matched nothing (rendered empty) AND the caller's positional
189
+ * argument was stored under that junk key and lost. Mirrors the Python master's
190
+ * _parse_macro_params. The default is null when none is declared.
191
+ */
192
+ static parseMacroParams(rawParams: string): Array<[string, string | null]>;
193
+ /**
194
+ * {% import "file" as alias %} -- load EVERY macro in a file under one namespace.
195
+ *
196
+ * The alias is bound as a plain object of macro functions, so {{ alias.greet(x) }}
197
+ * resolves through the engine's existing dotted-call path and each macro keeps the
198
+ * same argument binding, default handling and SafeString output as any other macro.
199
+ * A namespace object (not a class) is deliberate: a function stored as a class
200
+ * attribute binds as a method and would inject the namespace as the first argument,
201
+ * which is exactly the argument-shift bug the Python master carried (fixed there
202
+ * with types.SimpleNamespace). Both import forms must render identically.
203
+ */
204
+ private handleImportAs;
205
+ private handleFromImport;
206
+ private handleCache;
207
+ /**
208
+ * Handle {% live "name" poll N | sse | ws "path" [src "url"] %}...{% endlive %}.
209
+ *
210
+ * Server-rendered live region. The body renders once for first paint, is
211
+ * registered under <name> so GET /__frond/live/<name> (or a liveSource
212
+ * provider) can re-render it, and is wrapped in a marker element that
213
+ * frond.js wires to the chosen transport (poll / sse / ws). Mirrors the
214
+ * Python master's _handle_live and PHP/Ruby handleLive.
215
+ */
216
+ private handleLive;
217
+ /**
218
+ * Re-render a registered {% live %} fragment by name with fresh data.
219
+ * Returns the rendered HTML, or null if no fragment is registered under that
220
+ * name yet (its page has not rendered). GET /__frond/live/<name> calls this
221
+ * after resolving the provider data.
222
+ */
223
+ static renderLive(name: string, data?: Record<string, unknown>): string | null;
224
+ /** Register a data provider for a {% live %} block. Invoked with the live
225
+ * request on every refresh so auth re-applies. Mirrors Python's @live_source. */
226
+ static liveSource(name: string, fn: LiveProvider): void;
227
+ /** The provider registered for a live block, or null. */
228
+ static getLiveSource(name: string): LiveProvider | null;
229
+ /** Whether a live fragment has been registered (its page rendered). */
230
+ static hasLiveFragment(name: string): boolean;
231
+ /** The ws path a live block declared (data-ws), or null. */
232
+ static getLiveWsPath(name: string): string | null;
233
+ /**
234
+ * Resolve GET /__frond/live/{name}: run the provider with the live request
235
+ * (auth re-applies), re-render the fragment, and return a pure {status, body}
236
+ * descriptor the route handler applies to the response. 404 for an unknown
237
+ * name / unrendered fragment. Mirrors Python's live_endpoint / PHP respondLive.
238
+ */
239
+ static respondLive(req: LiveRequest, name: string): LiveResponse;
240
+ /** Wire the WebSocket broadcaster used by pushLive. Called once by @tina4/core
241
+ * at server boot (frond is a zero-dep leaf and cannot import core). */
242
+ static setLiveBroadcaster(fn: LiveBroadcaster | null): void;
243
+ /**
244
+ * Re-render the '<name>' live fragment and push it to connected clients.
245
+ * Broadcasts a {type,name,html} envelope over WebSocket to the block's
246
+ * declared data-ws path (else a room named <name>). Returns the rendered
247
+ * HTML, or null if the fragment is not registered. Mirrors Python push_live
248
+ * / PHP pushLive. The broadcast is best-effort — a missing/failed broadcaster
249
+ * never throws into the caller.
250
+ */
251
+ static pushLive(name: string, data?: Record<string, unknown>): string | null;
252
+ /**
253
+ * {% set name %}...{% endset %} -- render the body and bind it.
254
+ *
255
+ * Emits nothing itself. The captured value is a SafeString because it is
256
+ * template output that has already been escaped on the way in; re-escaping it
257
+ * at {{ name }} would double-encode every entity. Twig and Jinja2 both mark the
258
+ * capture safe. Returns the index just past {% endset %}.
259
+ */
260
+ private handleSetBlock;
261
+ private handleSpaceless;
262
+ private handleAutoescape;
263
+ }
@@ -0,0 +1,2 @@
1
+ export { Frond } from "./engine.js";
2
+ export type { FilterFn, TestFn, LiveProvider, LiveRequest, LiveResponse, LiveBroadcaster } from "./engine.js";
@@ -0,0 +1,183 @@
1
+ import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
2
+ /**
3
+ * Turn a URL path component into a Firebird database identifier.
4
+ *
5
+ * Firebird is the awkward one — it needs either an absolute file path on the
6
+ * server, a Windows drive-letter path, or an alias name. The classic URI form
7
+ * uses a double-slash to keep the leading "/" of an absolute path through
8
+ * URL parsing:
9
+ *
10
+ * firebird://host:port//firebird/data/app.fdb -> /firebird/data/app.fdb
11
+ *
12
+ * But that double slash is unintuitive to anyone used to the way
13
+ * postgres / mysql / mssql encode the database name. We accept five
14
+ * equivalent forms and normalise all of them:
15
+ *
16
+ * - `//abs/path/db.fdb` -> `/abs/path/db.fdb` (classic double-slash)
17
+ * - `/abs/path/db.fdb` -> `/abs/path/db.fdb` (single-slash, what most people type)
18
+ * - `/C:/Data/db.fdb` -> `C:/Data/db.fdb` (Windows, leading URL slash dropped)
19
+ * - `/C%3A/Data/db.fdb` -> `C:/Data/db.fdb` (Windows with URL-encoded colon)
20
+ * - `/employee` -> `employee` (alias — single token)
21
+ *
22
+ * Aliases are detected as the leftover case: a single token with no
23
+ * slashes. Anything path-like is kept as a path.
24
+ */
25
+ export declare function normalizeFirebirdDbIdentifier(rawPath: string): string;
26
+ /**
27
+ * Resolve the Firebird connection charset (php #160 / parity with the Python
28
+ * master's `_resolve_firebird_charset`).
29
+ *
30
+ * The adapter used to pass NO charset, deferring to the driver's implicit
31
+ * default, which double-encodes UTF-8 bytes stored under a legacy `NONE`
32
+ * database. This resolves the charset from, in precedence order:
33
+ *
34
+ * 1. the connection URL query — `firebird://host:port/path?charset=NONE`
35
+ * 2. an explicit `charset` on the FirebirdConfig object passed to the adapter
36
+ * 3. the `TINA4_DATABASE_CHARSET` environment variable
37
+ * 4. the `UTF8` default
38
+ *
39
+ * Pure config resolution over its inputs (URL string, explicit charset, env) —
40
+ * it opens NO connection, so it is unit-testable without a live server.
41
+ */
42
+ export declare function resolveFirebirdCharset(connectionString: string, explicitCharset?: string): string;
43
+ export interface FirebirdConfig {
44
+ host?: string;
45
+ port?: number;
46
+ user?: string;
47
+ password?: string;
48
+ database?: string;
49
+ role?: string;
50
+ pageSize?: number;
51
+ /** Connection charset. Overridden by a `?charset=` URL query; see resolveFirebirdCharset. */
52
+ charset?: string;
53
+ }
54
+ /**
55
+ * Quote an identifier the way Firebird actually stores it: UPPERCASE.
56
+ *
57
+ * Firebird folds an UNQUOTED identifier to upper case and treats a QUOTED one as
58
+ * case-sensitive. So after the ordinary `CREATE TABLE probe_t (...)` the table is
59
+ * PROBE_T, and `INSERT INTO "probe_t"` matches nothing:
60
+ *
61
+ * Dynamic SQL Error / Table unknown / probe_t
62
+ *
63
+ * That broke the insert path against every conventionally-created table, columns
64
+ * included. A name the caller has ALREADY quoted is passed through untouched,
65
+ * which is the escape hatch for a genuinely case-sensitive `CREATE TABLE "orders"`.
66
+ */
67
+ export declare function fbQuote(name: string): string;
68
+ /**
69
+ * Firebird's stored column name, folded back only when it was folded.
70
+ *
71
+ * Firebird's identifier folding is ASYMMETRIC. An unquoted `AS x` is stored
72
+ * UPPERCASE, so the driver hands back "X" where every other engine Tina4
73
+ * supports gives "x" — PostgreSQL folds to lower, and MySQL, SQLite and MSSQL
74
+ * preserve what you wrote. Portable code reading row.x broke on Firebird alone.
75
+ *
76
+ * A QUOTED `AS "MyCol"` is stored exactly as written, and that case is
77
+ * deliberate — the caller asked for it — so it is left alone. Folding
78
+ * unconditionally makes a mixed-case key unreachable, the same asymmetric trap
79
+ * that made tableExists miss quoted tables.
80
+ *
81
+ * So: fold back only a name carrying no lowercase letter, the only thing
82
+ * unquoted folding can produce. A quoted ALL-CAPS name is genuinely
83
+ * indistinguishable from a folded one and is lowercased too; that ambiguity is
84
+ * Firebird's, and it is the one spelling this cannot round-trip.
85
+ */
86
+ export declare function firebirdColumnName(raw: string): string;
87
+ export declare class FirebirdAdapter implements DatabaseAdapter {
88
+ private config;
89
+ private db;
90
+ private transaction;
91
+ private _lastInsertId;
92
+ constructor(config: FirebirdConfig | string);
93
+ /** Connect to Firebird. Must be called before using the adapter. */
94
+ connect(): Promise<void>;
95
+ private parseUrl;
96
+ private ensureConnected;
97
+ /** Translate SQL for Firebird dialect. */
98
+ translateSql(sql: string): string;
99
+ /**
100
+ * The handle every statement runs on. While an explicit transaction is open
101
+ * (startTransactionAsync set `this.transaction`), statements MUST run on that
102
+ * transaction object so they are undone by rollbackAsync() / persisted by
103
+ * commitAsync() — node-firebird's transaction exposes the same
104
+ * query()/execute() as the connection. With no transaction open we run on
105
+ * `this.db`, whose per-statement work auto-commits on the connection.
106
+ *
107
+ * This matches the Python master's contract (tina4_python/database/firebird.py):
108
+ * there, ALL statements run on the single connection and start_transaction()
109
+ * merely suppresses the per-statement autocommit in execute() so the batch
110
+ * stays open until commit()/rollback(). node-firebird has no such suppression
111
+ * hook — its `db.query/execute` always auto-commit — so the equivalent is to
112
+ * route statements through the transaction object instead. Same observable
113
+ * behaviour: an open transaction is atomic and rolls back cleanly.
114
+ *
115
+ * Previously every statement ran on `this.db` unconditionally, so the
116
+ * transaction created by startTransactionAsync() never saw a single statement
117
+ * — rollbackAsync() rolled back an EMPTY transaction and the already
118
+ * auto-committed write survived (silent no-op). Twin of the PHP pdo_firebird
119
+ * bug fixed in 3.13.86.
120
+ */
121
+ private statementHandle;
122
+ private queryPromise;
123
+ private executePromise;
124
+ execute(sql: string, params?: unknown[]): unknown;
125
+ executeMany(sql: string, paramsList: unknown[][]): {
126
+ totalAffected: number;
127
+ lastId?: number | bigint;
128
+ };
129
+ executeManyAsync(sql: string, paramsList: unknown[][]): Promise<{
130
+ totalAffected: number;
131
+ lastId?: number | bigint;
132
+ }>;
133
+ executeAsync(sql: string, params?: unknown[]): Promise<unknown>;
134
+ query<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[];
135
+ queryAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
136
+ /** Ensure BLOB columns are readable — node-firebird may return callback-based
137
+ * blob readers. Convert to Buffer. Regular buffers pass through unchanged. */
138
+ private decodeBlobs;
139
+ fetch<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): T[];
140
+ fetchAsync<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): Promise<T[]>;
141
+ fetchOne<T = Record<string, unknown>>(sql: string, params?: unknown[]): T | null;
142
+ fetchOneAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T | null>;
143
+ insert(table: string, data: Record<string, unknown> | Record<string, unknown>[]): DatabaseResult;
144
+ insertAsync(table: string, data: Record<string, unknown> | Record<string, unknown>[]): Promise<DatabaseResult>;
145
+ update(table: string, data: Record<string, unknown>, filter: Record<string, unknown>, params?: unknown[]): DatabaseResult;
146
+ updateAsync(table: string, data: Record<string, unknown>, filter: Record<string, unknown> | string, params?: unknown[]): Promise<DatabaseResult>;
147
+ delete(table: string, filter: Record<string, unknown>, params?: unknown[]): DatabaseResult;
148
+ deleteAsync(table: string, filter: Record<string, unknown> | string, params?: unknown[]): Promise<DatabaseResult>;
149
+ startTransaction(): void;
150
+ startTransactionAsync(): Promise<void>;
151
+ commit(): void;
152
+ commitAsync(): Promise<void>;
153
+ rollback(): void;
154
+ rollbackAsync(): Promise<void>;
155
+ getTables(): string[];
156
+ tablesAsync(): Promise<string[]>;
157
+ getColumns(table: string): ColumnInfo[];
158
+ columnsAsync(table: string): Promise<ColumnInfo[]>;
159
+ lastInsertId(): number | bigint | null;
160
+ close(): void;
161
+ tableExists(name: string): boolean;
162
+ /**
163
+ * Is this table present, under either spelling Firebird could have stored?
164
+ *
165
+ * Firebird's folding rule is ASYMMETRIC:
166
+ * CREATE TABLE foo -> stored as FOO (unquoted folds to UPPER)
167
+ * CREATE TABLE "Foo" -> stored as Foo (quoted keeps its case)
168
+ *
169
+ * So upper-casing is CORRECT for the unquoted case - the common one - and
170
+ * WRONG for a quoted mixed-case table, which is a real thing on Firebird.
171
+ * Dropping the upper-case would not fix that, it would invert which half is
172
+ * broken.
173
+ *
174
+ * tableExistsAsync("Foo") is genuinely AMBIGUOUS: the caller could mean the
175
+ * quoted `Foo` or the unquoted `FOO`. Match EITHER. Do not "simplify" this
176
+ * back to one comparison - that is the bug it replaces, where a quoted
177
+ * mixed-case table read as absent and createTableAsync's idempotency guard
178
+ * (below) never fired.
179
+ */
180
+ tableExistsAsync(name: string): Promise<boolean>;
181
+ createTable(name: string, columns: Record<string, FieldDefinition>): void;
182
+ createTableAsync(name: string, columns: Record<string, FieldDefinition>): Promise<void>;
183
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Tina4 MongoDB Adapter — uses the `mongodb` package (optional peer dependency).
3
+ *
4
+ * Install: npm install mongodb
5
+ * URL format: mongodb://host:port/dbname or mongodb+srv://user:pass@host/dbname
6
+ */
7
+ import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
8
+ export interface MongoConfig {
9
+ host?: string;
10
+ port?: number;
11
+ user?: string;
12
+ password?: string;
13
+ database?: string;
14
+ connectionString?: string;
15
+ }
16
+ export declare class MongodbAdapter implements DatabaseAdapter {
17
+ private config;
18
+ private client;
19
+ private db;
20
+ private session;
21
+ private _lastInsertId;
22
+ private _inTransaction;
23
+ private _connectionString;
24
+ private _dbName;
25
+ constructor(config: MongoConfig | string);
26
+ /** Connect to MongoDB. Must be called before using the adapter. */
27
+ connect(): Promise<void>;
28
+ private ensureConnected;
29
+ /** Execute a SQL-like statement translated to a MongoDB operation. */
30
+ execute(sql: string, params?: unknown[]): unknown;
31
+ executeAsync(sql: string, params?: unknown[]): Promise<unknown>;
32
+ executeMany(sql: string, paramsList: unknown[][]): {
33
+ totalAffected: number;
34
+ lastId?: number | bigint;
35
+ };
36
+ executeManyAsync(sql: string, paramsList: unknown[][]): Promise<{
37
+ totalAffected: number;
38
+ lastId?: number | bigint;
39
+ }>;
40
+ query<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[];
41
+ queryAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
42
+ fetch<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): T[];
43
+ fetchAsync<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): Promise<T[]>;
44
+ fetchOne<T = Record<string, unknown>>(sql: string, params?: unknown[]): T | null;
45
+ fetchOneAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T | null>;
46
+ insert(table: string, data: Record<string, unknown> | Record<string, unknown>[]): DatabaseResult;
47
+ insertAsync(table: string, data: Record<string, unknown> | Record<string, unknown>[]): Promise<DatabaseResult>;
48
+ update(table: string, data: Record<string, unknown>, filter: Record<string, unknown>): DatabaseResult;
49
+ updateAsync(table: string, data: Record<string, unknown>, filter: Record<string, unknown>): Promise<DatabaseResult>;
50
+ delete(table: string, filter: Record<string, unknown> | string | Record<string, unknown>[]): DatabaseResult;
51
+ deleteAsync(table: string, filter: Record<string, unknown> | string | Record<string, unknown>[]): Promise<DatabaseResult>;
52
+ startTransaction(): void;
53
+ startTransactionAsync(): Promise<void>;
54
+ commit(): void;
55
+ commitAsync(): Promise<void>;
56
+ rollback(): void;
57
+ rollbackAsync(): Promise<void>;
58
+ getTables(): string[];
59
+ tablesAsync(): Promise<string[]>;
60
+ getColumns(table: string): ColumnInfo[];
61
+ /**
62
+ * Infer column schema by sampling a document from the collection.
63
+ * MongoDB is schema-less; this returns field names and inferred JS types.
64
+ */
65
+ columnsAsync(table: string): Promise<ColumnInfo[]>;
66
+ lastInsertId(): number | bigint | null;
67
+ close(): void;
68
+ tableExists(name: string): boolean;
69
+ tableExistsAsync(name: string): Promise<boolean>;
70
+ createTable(name: string, columns: Record<string, FieldDefinition>): void;
71
+ /**
72
+ * Create a MongoDB collection with optional JSON schema validation derived
73
+ * from the Tina4 field definitions.
74
+ */
75
+ createTableAsync(name: string, columns: Record<string, FieldDefinition>): Promise<void>;
76
+ /** Get column info as a plain array (legacy migration support). */
77
+ getTableColumnsAsync(table: string): Promise<Array<{
78
+ name: string;
79
+ type: string;
80
+ }>>;
81
+ }