webspresso 0.0.106 → 0.0.108

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 (118) hide show
  1. package/.agents/AGENTS.md +4 -1
  2. package/.agents/CACHE.md +1 -0
  3. package/.agents/CONTEXT.md +152 -0
  4. package/.agents/ORM.md +74 -0
  5. package/.agents/SHUTDOWN.md +8 -1
  6. package/.agents/skills/webspresso-framework/SKILL.md +2 -1
  7. package/.agents/skills/webspresso-orm-and-database/SKILL.md +9 -0
  8. package/.agents/skills/webspresso-routing-and-api/SKILL.md +18 -2
  9. package/.agents/skills/webspresso-services/SKILL.md +36 -3
  10. package/AGENTS.md +6 -3
  11. package/bin/webspresso.js +17 -7
  12. package/core/app.js +45 -11
  13. package/core/applySchema.js +3 -72
  14. package/core/auth/index.js +4 -1
  15. package/core/auth/jwt.js +85 -13
  16. package/core/auth/magic-link.js +23 -5
  17. package/core/auth/manager.js +7 -1
  18. package/core/auth/redis-session.js +62 -25
  19. package/core/auth/session-manager.js +28 -2
  20. package/core/auth/sessions.js +182 -52
  21. package/core/cache/lru.js +10 -0
  22. package/core/cache/tagged-lru.js +4 -0
  23. package/core/compileSchema.js +3 -68
  24. package/core/compression/compressible.js +53 -6
  25. package/core/compression/negotiate.js +31 -12
  26. package/core/console/eval.js +4 -1
  27. package/core/content/service.js +1 -0
  28. package/core/context/app-context.js +201 -0
  29. package/core/context/index.js +55 -0
  30. package/core/context/request.js +241 -0
  31. package/core/errors/domain.js +20 -2
  32. package/core/errors/http.js +1 -0
  33. package/core/events/dispatcher.js +144 -15
  34. package/core/events/signal.js +9 -0
  35. package/core/health/probes.js +12 -2
  36. package/core/http/hono-adapter.js +1 -1
  37. package/core/http/index.js +10 -1
  38. package/core/http/pipeline.js +519 -334
  39. package/core/index.js +36 -2
  40. package/core/mail/mailer.js +1 -1
  41. package/core/orm/eager-loader.js +170 -55
  42. package/core/orm/index.js +25 -47
  43. package/core/orm/json-fields.js +44 -13
  44. package/core/orm/model.js +18 -1
  45. package/core/orm/query-builder.js +33 -15
  46. package/core/orm/repository.js +59 -12
  47. package/core/orm/schema-helpers.js +59 -0
  48. package/core/orm/scopes.js +54 -4
  49. package/core/orm/transaction.js +109 -14
  50. package/core/plugins/index.js +8 -0
  51. package/core/plugins/plugin-manager.js +1098 -0
  52. package/core/queue/manager.js +55 -27
  53. package/core/rate-limit/stores/memory.js +23 -10
  54. package/core/resilience/circuit-breaker.js +34 -14
  55. package/core/schema-cache.js +2 -64
  56. package/core/services/builtins/auth.js +273 -0
  57. package/core/services/builtins/exchange.js +229 -0
  58. package/core/services/builtins/file-manager.js +178 -0
  59. package/core/services/builtins/mail.js +134 -0
  60. package/core/services/builtins/media.js +196 -0
  61. package/core/services/builtins/system.js +219 -0
  62. package/core/services/discovery.js +133 -0
  63. package/core/services/executor.js +629 -0
  64. package/core/services/index.js +114 -0
  65. package/core/services/loader.js +161 -0
  66. package/core/services/memoize.js +408 -0
  67. package/core/services/registry.js +731 -0
  68. package/core/services/validator.js +424 -0
  69. package/core/shutdown/shutdown-manager.js +194 -114
  70. package/core/ssr/stream.js +109 -16
  71. package/core/stream/text-stream.js +45 -4
  72. package/core/studio/inspector.js +9 -1
  73. package/core/studio/server.js +24 -8
  74. package/core/utils/index.js +110 -0
  75. package/core/utils/string.js +77 -16
  76. package/core/utils/timeout.js +4 -0
  77. package/core/validation/apply.js +74 -0
  78. package/core/validation/cache.js +66 -0
  79. package/core/validation/compiler.js +77 -0
  80. package/core/validation/index.js +40 -1
  81. package/index.d.ts +249 -12
  82. package/index.js +68 -20
  83. package/package.json +18 -8
  84. package/plugins/admin-panel/client/parts/07-model-list.js +5 -2
  85. package/plugins/admin-panel/index.js +24 -11
  86. package/plugins/admin-panel/modules/user-management.js +3 -2
  87. package/plugins/csrf/index.js +2 -1
  88. package/plugins/dashboard/index.js +27 -7
  89. package/src/app-context.js +4 -188
  90. package/src/dev/typegen.js +1 -1
  91. package/src/file-router.js +12 -3
  92. package/src/plugin-manager.js +5 -861
  93. package/src/server.js +30 -34
  94. package/src/services/builtins/auth.js +2 -273
  95. package/src/services/builtins/exchange.js +2 -229
  96. package/src/services/builtins/file-manager.js +2 -178
  97. package/src/services/builtins/mail.js +2 -134
  98. package/src/services/builtins/media.js +2 -196
  99. package/src/services/builtins/system.js +2 -219
  100. package/src/services/discovery.js +2 -125
  101. package/src/services/executor.js +2 -362
  102. package/src/services/index.js +4 -60
  103. package/src/services/loader.js +2 -0
  104. package/src/services/memoize.js +2 -234
  105. package/src/services/registry.js +2 -314
  106. package/src/services/validator.js +2 -273
  107. package/core/utils/array.js +0 -12
  108. package/core/utils/base64url.js +0 -8
  109. package/core/utils/ensure-leading-slash.js +0 -6
  110. package/core/utils/escape-html.js +0 -7
  111. package/core/utils/escape-regexp.js +0 -7
  112. package/core/utils/get-client-ip.js +0 -6
  113. package/core/utils/hmac.js +0 -6
  114. package/core/utils/random.js +0 -9
  115. package/core/utils/safe-compare.js +0 -3
  116. package/core/utils/sha256.js +0 -6
  117. package/core/utils/strip-trailing-slash.js +0 -6
  118. package/core/utils/walk.js +0 -7
package/core/index.js CHANGED
@@ -11,9 +11,11 @@
11
11
  const { CoreApp, createCoreApp } = require('./app');
12
12
  const {
13
13
  createServiceRegistry,
14
+ createServiceRegistryAsync,
14
15
  defineService,
15
16
  ServiceRegistry,
16
17
  executeService,
18
+ composeMiddleware,
17
19
  discoverServices,
18
20
  filePathToServiceName,
19
21
  toCamelCase,
@@ -23,7 +25,10 @@ const {
23
25
  stableCacheKey,
24
26
  sanitizeInput,
25
27
  maskSensitiveData,
26
- } = require('../src/services');
28
+ loadModuleSync,
29
+ loadModuleAsync,
30
+ extractModuleExport,
31
+ } = require('./services');
27
32
 
28
33
  const { ShutdownManager } = require('./shutdown');
29
34
  const errors = require('./errors');
@@ -37,7 +42,7 @@ const {
37
42
  createPluginManager,
38
43
  getPluginManager,
39
44
  resetPluginManager,
40
- } = require('../src/plugin-manager');
45
+ } = require('./plugins');
41
46
 
42
47
  // Lazy-loaded ORM accessor
43
48
  let _orm = null;
@@ -53,12 +58,37 @@ module.exports = {
53
58
  CoreApp,
54
59
  createCoreApp,
55
60
 
61
+ // Context Management Layer (App & Ambient Request Context)
62
+ context: require('./context'),
63
+ appContext: require('./context').appContext,
64
+ requestContext: require('./context').requestContext,
65
+ runWithContext: require('./context').runWithContext,
66
+ withContext: require('./context').withContext,
67
+ getContext: require('./context').getContext,
68
+ useContext: require('./context').useContext,
69
+ useDb: require('./context').useDb,
70
+ useAuth: require('./context').useAuth,
71
+ useUser: require('./context').useUser,
72
+ useLogger: require('./context').useLogger,
73
+ useReq: require('./context').useReq,
74
+ useRes: require('./context').useRes,
75
+ useParams: require('./context').useParams,
76
+ useQuery: require('./context').useQuery,
77
+ useService: require('./context').useService,
78
+ useQueue: require('./context').useQueue,
79
+ useCache: require('./context').useCache,
80
+ useBreaker: require('./context').useBreaker,
81
+ useTransaction: require('./context').useTransaction,
82
+
56
83
  // Services Layer
84
+ services: require('./services'),
57
85
  createServiceRegistry,
86
+ createServiceRegistryAsync,
58
87
  defineService,
59
88
  service: defineService,
60
89
  ServiceRegistry,
61
90
  executeService,
91
+ composeMiddleware,
62
92
  discoverServices,
63
93
  filePathToServiceName,
64
94
  toCamelCase,
@@ -68,6 +98,9 @@ module.exports = {
68
98
  stableCacheKey,
69
99
  sanitizeInput,
70
100
  maskSensitiveData,
101
+ loadModuleSync,
102
+ loadModuleAsync,
103
+ extractModuleExport,
71
104
 
72
105
  // Route & Endpoint Declarations
73
106
  defineApi: require('../src/api/define-api').defineApi,
@@ -296,6 +329,7 @@ Object.defineProperties(module.exports, {
296
329
  executeApiPipeline: { get: () => require('./http').executeApiPipeline, enumerable: true, configurable: true },
297
330
  executePagePipeline: { get: () => require('./http').executePagePipeline, enumerable: true, configurable: true },
298
331
  runMiddlewares: { get: () => require('./http').runMiddlewares, enumerable: true, configurable: true },
332
+ createPipelineRunner: { get: () => require('./http').createPipelineRunner, enumerable: true, configurable: true },
299
333
  enrichRouteError: { get: () => require('./http').enrichRouteError, enumerable: true, configurable: true },
300
334
 
301
335
  // Authentication & Verification
@@ -67,7 +67,7 @@ class Mailer {
67
67
 
68
68
  // Fallback simple text from HTML if text not provided
69
69
  if (!msg.text && typeof msg.html === 'string') {
70
- msg.text = msg.html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
70
+ msg.text = msg.html.replace(/<[^<>]*>/g, ' ').replace(/\s+/g, ' ').trim();
71
71
  }
72
72
 
73
73
  try {
@@ -8,6 +8,60 @@ const { resolveRelationModel, getRelationKeys } = require('./model');
8
8
  const { applyScopes, createScopeContext } = require('./scopes');
9
9
  const { getJsonColumns, deserializeJsonFields } = require('./json-fields');
10
10
 
11
+ const RELATION_TREE_CACHE_LIMIT = 512;
12
+ const relationTreeCache = new Map();
13
+
14
+ /**
15
+ * Generate a deterministic cache key for relation names specifications
16
+ * @param {string|Array|Object} relationNames
17
+ * @returns {string|null}
18
+ */
19
+ function getRelationTreeCacheKey(relationNames) {
20
+ if (typeof relationNames === 'string') {
21
+ return 's:' + relationNames;
22
+ }
23
+ if (Array.isArray(relationNames)) {
24
+ for (let i = 0; i < relationNames.length; i++) {
25
+ if (typeof relationNames[i] !== 'string') {
26
+ try {
27
+ return 'o:' + JSON.stringify(relationNames);
28
+ } catch (_) {
29
+ return null;
30
+ }
31
+ }
32
+ }
33
+ return 'a:' + relationNames.join('|');
34
+ }
35
+ if (relationNames && typeof relationNames === 'object') {
36
+ try {
37
+ return 'o:' + JSON.stringify(relationNames);
38
+ } catch (_) {
39
+ return null;
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+
45
+ /**
46
+ * Clone a relation tree Map to prevent external mutation from poisoning cache
47
+ * @param {Map<string, string[]>} tree
48
+ * @returns {Map<string, string[]>}
49
+ */
50
+ function cloneRelationTree(tree) {
51
+ const copy = new Map();
52
+ for (const [k, v] of tree.entries()) {
53
+ copy.set(k, v.slice());
54
+ }
55
+ return copy;
56
+ }
57
+
58
+ /**
59
+ * Clear the internal relation tree cache
60
+ */
61
+ function clearRelationTreeCache() {
62
+ relationTreeCache.clear();
63
+ }
64
+
11
65
  /**
12
66
  * Parse relation specifications into a grouped structure: Map<topLevelRelation, string[]>
13
67
  * Supports dot notation ('author.profile'), arrays (['author', 'comments.author']),
@@ -17,6 +71,21 @@ const { getJsonColumns, deserializeJsonFields } = require('./json-fields');
17
71
  * @returns {Map<string, string[]>}
18
72
  */
19
73
  function parseRelationTree(relationNames) {
74
+ if (relationNames instanceof Map) {
75
+ return cloneRelationTree(relationNames);
76
+ }
77
+
78
+ const cacheKey = getRelationTreeCacheKey(relationNames);
79
+ if (cacheKey) {
80
+ const cached = relationTreeCache.get(cacheKey);
81
+ if (cached) {
82
+ // Refresh LRU order
83
+ relationTreeCache.delete(cacheKey);
84
+ relationTreeCache.set(cacheKey, cached);
85
+ return cloneRelationTree(cached);
86
+ }
87
+ }
88
+
20
89
  const tree = new Map();
21
90
 
22
91
  function addPath(top, nested) {
@@ -81,6 +150,15 @@ function parseRelationTree(relationNames) {
81
150
  processItem(item);
82
151
  }
83
152
 
153
+ if (cacheKey) {
154
+ if (relationTreeCache.size >= RELATION_TREE_CACHE_LIMIT) {
155
+ const firstKey = relationTreeCache.keys().next().value;
156
+ relationTreeCache.delete(firstKey);
157
+ }
158
+ relationTreeCache.set(cacheKey, tree);
159
+ return cloneRelationTree(tree);
160
+ }
161
+
84
162
  return tree;
85
163
  }
86
164
 
@@ -93,74 +171,104 @@ function parseRelationTree(relationNames) {
93
171
  * @param {import('./types').ScopeContext} [scopeContext] - Scope context
94
172
  * @returns {Promise<Object[]>} Records with relations attached
95
173
  */
96
- async function loadRelations(records, relationNames, model, knex, scopeContext) {
97
- if (!records || !records.length || !relationNames || !relationNames.length) {
98
- return records;
174
+ /**
175
+ * Helper to load a single top-level relation and its nested relations
176
+ * @private
177
+ */
178
+ async function loadSingleRelation(records, relationName, nestedPaths, model, knex, context) {
179
+ const relation = model.relations?.[relationName];
180
+ if (!relation) {
181
+ console.warn(`Relation "${relationName}" not found on model "${model.name}"`);
182
+ return;
99
183
  }
100
184
 
101
- const context = scopeContext || createScopeContext();
102
- const tree = parseRelationTree(relationNames);
103
-
104
- // Process each relation grouped by top-level
105
- for (const [relationName, nestedPaths] of tree.entries()) {
106
- const relation = model.relations?.[relationName];
107
- if (!relation) {
108
- console.warn(`Relation "${relationName}" not found on model "${model.name}"`);
109
- continue;
110
- }
185
+ const { localKey, foreignKey, relatedModel } = getRelationKeys(model, relationName);
186
+
187
+ switch (relation.type) {
188
+ case 'belongsTo':
189
+ await loadBelongsTo(records, relationName, localKey, foreignKey, relatedModel, knex, context);
190
+ break;
191
+ case 'hasMany':
192
+ await loadHasMany(records, relationName, localKey, foreignKey, relatedModel, knex, context);
193
+ break;
194
+ case 'hasOne':
195
+ await loadHasOne(records, relationName, localKey, foreignKey, relatedModel, knex, context);
196
+ break;
197
+ default:
198
+ console.warn(`Unsupported relation type "${relation.type}" for relation "${relationName}" on model "${model.name}"`);
199
+ return;
200
+ }
111
201
 
112
- const { localKey, foreignKey, relatedModel } = getRelationKeys(model, relationName);
113
-
114
- switch (relation.type) {
115
- case 'belongsTo':
116
- await loadBelongsTo(records, relationName, localKey, foreignKey, relatedModel, knex, context);
117
- break;
118
- case 'hasMany':
119
- await loadHasMany(records, relationName, localKey, foreignKey, relatedModel, knex, context);
120
- break;
121
- case 'hasOne':
122
- await loadHasOne(records, relationName, localKey, foreignKey, relatedModel, knex, context);
123
- break;
124
- default:
125
- console.warn(`Unsupported relation type "${relation.type}" for relation "${relationName}" on model "${model.name}"`);
126
- continue;
127
- }
202
+ // Process nested relations if any
203
+ if (nestedPaths.length > 0) {
204
+ const childRecords = [];
205
+ const seenRefs = new Set();
206
+ const len = records.length;
128
207
 
129
- // Process nested relations if any
130
- if (nestedPaths.length > 0) {
131
- const childRecords = [];
132
- const seenRefs = new Set();
133
- const len = records.length;
134
-
135
- for (let i = 0; i < len; i++) {
136
- const child = records[i][relationName];
137
- if (!child) continue;
138
-
139
- if (Array.isArray(child)) {
140
- for (let j = 0; j < child.length; j++) {
141
- const item = child[j];
142
- if (item && typeof item === 'object' && !seenRefs.has(item)) {
143
- seenRefs.add(item);
144
- childRecords.push(item);
145
- }
208
+ for (let i = 0; i < len; i++) {
209
+ const child = records[i][relationName];
210
+ if (!child) continue;
211
+
212
+ if (Array.isArray(child)) {
213
+ for (let j = 0; j < child.length; j++) {
214
+ const item = child[j];
215
+ if (item && typeof item === 'object' && !seenRefs.has(item)) {
216
+ seenRefs.add(item);
217
+ childRecords.push(item);
146
218
  }
147
- } else if (typeof child === 'object' && !seenRefs.has(child)) {
148
- seenRefs.add(child);
149
- childRecords.push(child);
150
219
  }
220
+ } else if (typeof child === 'object' && !seenRefs.has(child)) {
221
+ seenRefs.add(child);
222
+ childRecords.push(child);
151
223
  }
224
+ }
152
225
 
153
- if (childRecords.length > 0) {
154
- await loadRelations(childRecords, nestedPaths, relatedModel, knex, context);
155
- }
226
+ if (childRecords.length > 0) {
227
+ await loadRelations(childRecords, nestedPaths, relatedModel, knex, context);
156
228
  }
157
229
  }
230
+ }
231
+
232
+ /**
233
+ * Load relations for a set of records using batch queries (supports nested relations)
234
+ * Executes independent top-level relations concurrently via Promise.all.
235
+ *
236
+ * @param {Object[]} records - Records to load relations for
237
+ * @param {Array<string|Object>} relationNames - Names of relations to load (e.g. ['author.profile', 'comments.author'])
238
+ * @param {import('./types').ModelDefinition} model - Model definition
239
+ * @param {import('knex').Knex|import('knex').Knex.Transaction} knex - Knex instance
240
+ * @param {import('./types').ScopeContext} [scopeContext] - Scope context
241
+ * @returns {Promise<Object[]>} Records with relations attached
242
+ */
243
+ async function loadRelations(records, relationNames, model, knex, scopeContext) {
244
+ if (!records || !records.length || !relationNames || !relationNames.length) {
245
+ return records;
246
+ }
247
+
248
+ const context = scopeContext || createScopeContext();
249
+ const tree = relationNames instanceof Map ? relationNames : parseRelationTree(relationNames);
250
+ const entries = Array.from(tree.entries());
251
+
252
+ if (entries.length === 1) {
253
+ // Fast path: single top-level relation without Promise.all allocation
254
+ const [relationName, nestedPaths] = entries[0];
255
+ await loadSingleRelation(records, relationName, nestedPaths, model, knex, context);
256
+ } else if (entries.length > 1) {
257
+ // Concurrent execution for independent top-level relations (eliminates sequential waterfalls)
258
+ await Promise.all(
259
+ entries.map(([relationName, nestedPaths]) =>
260
+ loadSingleRelation(records, relationName, nestedPaths, model, knex, context)
261
+ )
262
+ );
263
+ }
158
264
 
159
265
  return records;
160
266
  }
161
267
 
162
268
  /**
163
269
  * Execute whereIn query in chunks to prevent parameter limits and query planner degradation
270
+ * Executes multiple chunks concurrently via Promise.all.
271
+ *
164
272
  * @param {import('knex').Knex} knex
165
273
  * @param {string} table
166
274
  * @param {string} column
@@ -177,13 +285,18 @@ async function queryInChunks(knex, table, column, values, scopeContext, model, c
177
285
  return await qb;
178
286
  }
179
287
 
180
- const results = [];
288
+ const chunkPromises = [];
181
289
  for (let i = 0; i < values.length; i += chunkSize) {
182
290
  const slice = values.slice(i, i + chunkSize);
183
291
  let qb = knex(table).whereIn(column, slice);
184
292
  qb = applyScopes(qb, scopeContext, model);
185
- const chunkResults = await qb;
186
- results.push(...chunkResults);
293
+ chunkPromises.push(qb);
294
+ }
295
+
296
+ const chunkResults = await Promise.all(chunkPromises);
297
+ const results = [];
298
+ for (let i = 0; i < chunkResults.length; i++) {
299
+ results.push(...chunkResults[i]);
187
300
  }
188
301
  return results;
189
302
  }
@@ -402,4 +515,6 @@ module.exports = {
402
515
  loadHasOne,
403
516
  queryInChunks,
404
517
  parseRelationTree,
518
+ clearRelationTreeCache,
519
+ getRelationTreeCacheKey,
405
520
  };
package/core/orm/index.js CHANGED
@@ -9,6 +9,7 @@ const fs = require('fs');
9
9
  const { createSchemaHelpers, extractColumnsFromSchema, getColumnMeta } = require('./schema-helpers');
10
10
  const { defineModel, getModel, getAllModels, hasModel, clearRegistry, resolveModelName, isCascadeEnabled } = require('./model');
11
11
  const { createRepository } = require('./repository');
12
+ const { createQueryBuilder, QueryBuilder } = require('./query-builder');
12
13
  const { createMigrationManager } = require('./migrations');
13
14
  const { createSeeder } = require('./seeder');
14
15
  const { createScopeContext } = require('./scopes');
@@ -278,9 +279,8 @@ function createDatabase(config) {
278
279
  */
279
280
  function query(modelName, scopeContext) {
280
281
  const model = getModelInstance(modelName);
281
- const ctx = scopeContext || createScopeContext();
282
- const repo = createRepository(model, knexInstance, ctx, ormCacheLayer);
283
- return repo.query();
282
+ const ctx = scopeContext ? { ...scopeContext } : createScopeContext();
283
+ return createQueryBuilder(model, knexInstance, ctx, ormCacheLayer);
284
284
  }
285
285
 
286
286
  /**
@@ -309,40 +309,7 @@ function createDatabase(config) {
309
309
  * @returns {Promise<*>} Result of callback
310
310
  */
311
311
  async function transaction(callback, scopeContext) {
312
- const ambientTrx = getAmbientTransaction();
313
- const runner = (ambientTrx && typeof ambientTrx.transaction === 'function') ? ambientTrx : knexInstance;
314
-
315
- return runner.transaction(async (trx) => {
316
- let currentScope = scopeContext ? { ...scopeContext } : createScopeContext();
317
- const trxContext = {
318
- trx,
319
- getRepository(modelName, sContext) {
320
- const model = getModelInstance(modelName);
321
- const ctx = sContext || currentScope;
322
- return createRepository(model, trx, ctx, ormCacheLayer);
323
- },
324
- createRepository(model, sContext) {
325
- const ctx = sContext || currentScope;
326
- return createRepository(model, trx, ctx, ormCacheLayer);
327
- },
328
- forTenant(tenantId) {
329
- currentScope = { ...currentScope, tenantId };
330
- return this;
331
- },
332
- forUser(user) {
333
- currentScope = { ...currentScope, user };
334
- return this;
335
- },
336
- withoutRls() {
337
- currentScope = { ...currentScope, rlsDisabled: true };
338
- return this;
339
- },
340
- getScopeContext() {
341
- return { ...currentScope };
342
- },
343
- };
344
- return runWithAmbientTransaction(trx, () => callback(trxContext));
345
- });
312
+ return runTransaction(knexInstance, callback, scopeContext, ormCacheLayer, getModelInstance);
346
313
  }
347
314
 
348
315
  /**
@@ -472,6 +439,9 @@ const zdb = z ? createSchemaHelpers(z) : null;
472
439
  module.exports = {
473
440
  // Main factory
474
441
  createDatabase,
442
+ // Query builder
443
+ createQueryBuilder,
444
+ QueryBuilder,
475
445
  // ORM cache (provider + layer utilities)
476
446
  createMemoryCacheProvider: require('./cache/memory-provider').createMemoryCacheProvider,
477
447
  OrmCacheLayer: require('./cache/layer').OrmCacheLayer,
@@ -485,6 +455,8 @@ module.exports = {
485
455
  runTransaction,
486
456
  // Schema helpers
487
457
  zdb,
458
+ z,
459
+ zod: z,
488
460
  createSchemaHelpers,
489
461
  // Model utilities
490
462
  defineModel,
@@ -511,6 +483,7 @@ module.exports = {
511
483
  loadRelations: require('./eager-loader').loadRelations,
512
484
  loadRelation: require('./eager-loader').loadRelation,
513
485
  parseRelationTree: require('./eager-loader').parseRelationTree,
486
+ clearRelationTreeCache: require('./eager-loader').clearRelationTreeCache,
514
487
  // Query Complexity & DoS Protection
515
488
  QueryComplexityError: require('./complexity').QueryComplexityError,
516
489
  validateQueryComplexity: require('./complexity').validateQueryComplexity,
@@ -543,11 +516,16 @@ module.exports = {
543
516
  getUserId: require('./scopes').getUserId,
544
517
  getTenantId: require('./scopes').getTenantId,
545
518
  createScopeContext: require('./scopes').createScopeContext,
546
- // Cloudflare D1 Native Edge Dialect
547
- createD1Knex: require('./d1').createD1Knex,
548
- Client_D1: require('./d1').Client_D1,
549
- executeD1Batch: require('./d1').executeD1Batch,
550
- normalizeD1Bindings: require('./d1').normalizeD1Bindings,
519
+ getSoftDeleteColumn: require('./scopes').getSoftDeleteColumn,
520
+ getCreatedAtColumn: require('./scopes').getCreatedAtColumn,
521
+ getUpdatedAtColumn: require('./scopes').getUpdatedAtColumn,
522
+ applyInsertTimestamps: require('./scopes').applyInsertTimestamps,
523
+ applyUpdateTimestamps: require('./scopes').applyUpdateTimestamps,
524
+ // Cloudflare D1 Native Edge Dialect (lazy getters to avoid eager dialect loading)
525
+ get createD1Knex() { return require('./d1').createD1Knex; },
526
+ get Client_D1() { return require('./d1').Client_D1; },
527
+ get executeD1Batch() { return require('./d1').executeD1Batch; },
528
+ get normalizeD1Bindings() { return require('./d1').normalizeD1Bindings; },
551
529
  // Serverless Connection Pool Management
552
530
  isServerlessEnvironment,
553
531
  normalizeServerlessPoolConfig,
@@ -555,9 +533,9 @@ module.exports = {
555
533
  isServerlessEnvironment,
556
534
  normalizeServerlessPoolConfig,
557
535
  },
558
- // Cloud Database Migrations
559
- waitForDatabaseConnection: require('./migrate-cloud').waitForDatabaseConnection,
560
- freeStaleMigrationLock: require('./migrate-cloud').freeStaleMigrationLock,
561
- getMigrationStatus: require('./migrate-cloud').getMigrationStatus,
562
- runCloudMigration: require('./migrate-cloud').runCloudMigration,
536
+ // Cloud Database Migrations (lazy getters)
537
+ get waitForDatabaseConnection() { return require('./migrate-cloud').waitForDatabaseConnection; },
538
+ get freeStaleMigrationLock() { return require('./migrate-cloud').freeStaleMigrationLock; },
539
+ get getMigrationStatus() { return require('./migrate-cloud').getMigrationStatus; },
540
+ get runCloudMigration() { return require('./migrate-cloud').runCloudMigration; },
563
541
  };
@@ -3,8 +3,6 @@
3
3
  * @module core/orm/json-fields
4
4
  */
5
5
 
6
- const { safeJsonParse } = require('../utils/json');
7
-
8
6
  /**
9
7
  * Get JSON column names from model
10
8
  * @param {import('./types').ModelDefinition} model - Model definition
@@ -12,6 +10,14 @@ const { safeJsonParse } = require('../utils/json');
12
10
  */
13
11
  function getJsonColumns(model) {
14
12
  if (model && model._jsonColumns) {
13
+ if (!model._jsonColumns._array) {
14
+ Object.defineProperty(model._jsonColumns, '_array', {
15
+ value: Array.from(model._jsonColumns),
16
+ writable: true,
17
+ configurable: true,
18
+ enumerable: false,
19
+ });
20
+ }
15
21
  return model._jsonColumns;
16
22
  }
17
23
  const jsonCols = new Set();
@@ -40,6 +46,12 @@ function getJsonColumns(model) {
40
46
  }
41
47
  }
42
48
  }
49
+ Object.defineProperty(jsonCols, '_array', {
50
+ value: Array.from(jsonCols),
51
+ writable: true,
52
+ configurable: true,
53
+ enumerable: false,
54
+ });
43
55
  if (model) {
44
56
  model._jsonColumns = jsonCols;
45
57
  }
@@ -49,17 +61,22 @@ function getJsonColumns(model) {
49
61
  /**
50
62
  * Serialize JSON fields for database storage
51
63
  * @param {Object} data - Data to serialize
52
- * @param {Set<string>} jsonColumns - JSON column names
64
+ * @param {Set<string>|Array<string>} jsonColumns - JSON column names
53
65
  * @returns {Object} Serialized data
54
66
  */
55
67
  function serializeJsonFields(data, jsonColumns) {
56
- if (jsonColumns.size === 0) return data;
68
+ if (!data || !jsonColumns) return data;
69
+ const size = jsonColumns.size !== undefined ? jsonColumns.size : jsonColumns.length;
70
+ if (size === 0) return data;
57
71
 
72
+ const cols = jsonColumns._array || (Array.isArray(jsonColumns) ? jsonColumns : Array.from(jsonColumns));
58
73
  const serialized = { ...data };
59
- for (const col of jsonColumns) {
60
- if (col in serialized && serialized[col] !== null && serialized[col] !== undefined) {
61
- if (typeof serialized[col] !== 'string') {
62
- serialized[col] = JSON.stringify(serialized[col]);
74
+ for (let i = 0; i < cols.length; i++) {
75
+ const col = cols[i];
76
+ if (col in serialized) {
77
+ const val = serialized[col];
78
+ if (val !== null && val !== undefined && typeof val !== 'string') {
79
+ serialized[col] = JSON.stringify(val);
63
80
  }
64
81
  }
65
82
  }
@@ -69,15 +86,28 @@ function serializeJsonFields(data, jsonColumns) {
69
86
  /**
70
87
  * Deserialize JSON fields from database
71
88
  * @param {Object} record - Record from database
72
- * @param {Set<string>} jsonColumns - JSON column names
89
+ * @param {Set<string>|Array<string>} jsonColumns - JSON column names
73
90
  * @returns {Object} Deserialized record
74
91
  */
75
92
  function deserializeJsonFields(record, jsonColumns) {
76
- if (!record || jsonColumns.size === 0) return record;
93
+ if (!record || !jsonColumns) return record;
94
+ const size = jsonColumns.size !== undefined ? jsonColumns.size : jsonColumns.length;
95
+ if (size === 0) return record;
77
96
 
78
- for (const col of jsonColumns) {
79
- if (col in record && record[col] !== null && record[col] !== undefined) {
80
- record[col] = safeJsonParse(record[col], record[col]);
97
+ const cols = jsonColumns._array || (Array.isArray(jsonColumns) ? jsonColumns : Array.from(jsonColumns));
98
+ for (let i = 0; i < cols.length; i++) {
99
+ const col = cols[i];
100
+ if (col in record) {
101
+ const val = record[col];
102
+ // If already parsed (object, array, number, boolean) or null/undefined, skip parsing
103
+ if (typeof val === 'string') {
104
+ if (val.length === 0) continue;
105
+ try {
106
+ record[col] = JSON.parse(val);
107
+ } catch (_) {
108
+ // Keep raw string if parsing fails
109
+ }
110
+ }
81
111
  }
82
112
  }
83
113
  return record;
@@ -88,3 +118,4 @@ module.exports = {
88
118
  serializeJsonFields,
89
119
  deserializeJsonFields,
90
120
  };
121
+
package/core/orm/model.js CHANGED
@@ -8,6 +8,7 @@ const z = require('zod');
8
8
  const { trimUrlPathSlashes } = require('../url-path-normalize');
9
9
  const { createSchemaHelpers, extractColumnsFromSchema } = require('./schema-helpers');
10
10
  const { ModelEvents, Hooks } = require('./events');
11
+ const { getJsonColumns } = require('./json-fields');
11
12
  const zdb = z ? createSchemaHelpers(z) : null;
12
13
 
13
14
  /**
@@ -22,7 +23,7 @@ const modelRegistry = new Map();
22
23
  * @returns {import('./types').ModelDefinition}
23
24
  */
24
25
  function defineModel(options) {
25
- const {
26
+ let {
26
27
  name,
27
28
  table,
28
29
  schema,
@@ -43,6 +44,11 @@ function defineModel(options) {
43
44
  options: nestedOptions = {},
44
45
  } = options;
45
46
 
47
+ // Support functional schema definitions: schema: ({ zdb, z }) => ({ ... })
48
+ if (typeof schema === 'function') {
49
+ schema = schema({ zdb, z: zdb?.z || z });
50
+ }
51
+
46
52
  // Validate required fields
47
53
  if (!name || typeof name !== 'string') {
48
54
  throw new Error('Model name is required and must be a string');
@@ -193,6 +199,11 @@ function defineModel(options) {
193
199
  ? rlsConfig
194
200
  : null;
195
201
 
202
+ const hasShape = normalizedSchema && normalizedSchema.shape;
203
+ const resolvedDeletedCol = ((columns && columns.has('deletedAt')) || (hasShape && normalizedSchema.shape.deletedAt)) ? 'deletedAt' : 'deleted_at';
204
+ const resolvedCreatedCol = ((columns && columns.has('createdAt')) || (hasShape && normalizedSchema.shape.createdAt)) ? 'createdAt' : 'created_at';
205
+ const resolvedUpdatedCol = ((columns && columns.has('updatedAt')) || (hasShape && normalizedSchema.shape.updatedAt)) ? 'updatedAt' : 'updated_at';
206
+
196
207
  // Create model definition
197
208
  const model = {
198
209
  name,
@@ -200,6 +211,9 @@ function defineModel(options) {
200
211
  schema: normalizedSchema,
201
212
  _partialSchema: typeof normalizedSchema?.partial === 'function' ? normalizedSchema.partial() : normalizedSchema,
202
213
  primaryKey,
214
+ _deletedCol: resolvedDeletedCol,
215
+ _createdCol: resolvedCreatedCol,
216
+ _updatedCol: resolvedUpdatedCol,
203
217
  relations: normalizedRelations,
204
218
  scopes: {
205
219
  softDelete: Boolean(softDeleteConfig),
@@ -259,6 +273,9 @@ function defineModel(options) {
259
273
  cache: cacheOption,
260
274
  };
261
275
 
276
+ // Precompute JSON column metadata
277
+ getJsonColumns(model);
278
+
262
279
  // Register model
263
280
  modelRegistry.set(name, model);
264
281