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.
- package/.agents/AGENTS.md +4 -1
- package/.agents/CACHE.md +1 -0
- package/.agents/CONTEXT.md +152 -0
- package/.agents/ORM.md +74 -0
- package/.agents/SHUTDOWN.md +8 -1
- package/.agents/skills/webspresso-framework/SKILL.md +2 -1
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +9 -0
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +18 -2
- package/.agents/skills/webspresso-services/SKILL.md +36 -3
- package/AGENTS.md +6 -3
- package/bin/webspresso.js +17 -7
- package/core/app.js +45 -11
- package/core/applySchema.js +3 -72
- package/core/auth/index.js +4 -1
- package/core/auth/jwt.js +85 -13
- package/core/auth/magic-link.js +23 -5
- package/core/auth/manager.js +7 -1
- package/core/auth/redis-session.js +62 -25
- package/core/auth/session-manager.js +28 -2
- package/core/auth/sessions.js +182 -52
- package/core/cache/lru.js +10 -0
- package/core/cache/tagged-lru.js +4 -0
- package/core/compileSchema.js +3 -68
- package/core/compression/compressible.js +53 -6
- package/core/compression/negotiate.js +31 -12
- package/core/console/eval.js +4 -1
- package/core/content/service.js +1 -0
- package/core/context/app-context.js +201 -0
- package/core/context/index.js +55 -0
- package/core/context/request.js +241 -0
- package/core/errors/domain.js +20 -2
- package/core/errors/http.js +1 -0
- package/core/events/dispatcher.js +144 -15
- package/core/events/signal.js +9 -0
- package/core/health/probes.js +12 -2
- package/core/http/hono-adapter.js +1 -1
- package/core/http/index.js +10 -1
- package/core/http/pipeline.js +519 -334
- package/core/index.js +36 -2
- package/core/mail/mailer.js +1 -1
- package/core/orm/eager-loader.js +170 -55
- package/core/orm/index.js +25 -47
- package/core/orm/json-fields.js +44 -13
- package/core/orm/model.js +18 -1
- package/core/orm/query-builder.js +33 -15
- package/core/orm/repository.js +59 -12
- package/core/orm/schema-helpers.js +59 -0
- package/core/orm/scopes.js +54 -4
- package/core/orm/transaction.js +109 -14
- package/core/plugins/index.js +8 -0
- package/core/plugins/plugin-manager.js +1098 -0
- package/core/queue/manager.js +55 -27
- package/core/rate-limit/stores/memory.js +23 -10
- package/core/resilience/circuit-breaker.js +34 -14
- package/core/schema-cache.js +2 -64
- package/core/services/builtins/auth.js +273 -0
- package/core/services/builtins/exchange.js +229 -0
- package/core/services/builtins/file-manager.js +178 -0
- package/core/services/builtins/mail.js +134 -0
- package/core/services/builtins/media.js +196 -0
- package/core/services/builtins/system.js +219 -0
- package/core/services/discovery.js +133 -0
- package/core/services/executor.js +629 -0
- package/core/services/index.js +114 -0
- package/core/services/loader.js +161 -0
- package/core/services/memoize.js +408 -0
- package/core/services/registry.js +731 -0
- package/core/services/validator.js +424 -0
- package/core/shutdown/shutdown-manager.js +194 -114
- package/core/ssr/stream.js +109 -16
- package/core/stream/text-stream.js +45 -4
- package/core/studio/inspector.js +9 -1
- package/core/studio/server.js +24 -8
- package/core/utils/index.js +110 -0
- package/core/utils/string.js +77 -16
- package/core/utils/timeout.js +4 -0
- package/core/validation/apply.js +74 -0
- package/core/validation/cache.js +66 -0
- package/core/validation/compiler.js +77 -0
- package/core/validation/index.js +40 -1
- package/index.d.ts +249 -12
- package/index.js +68 -20
- package/package.json +18 -8
- package/plugins/admin-panel/client/parts/07-model-list.js +5 -2
- package/plugins/admin-panel/index.js +24 -11
- package/plugins/admin-panel/modules/user-management.js +3 -2
- package/plugins/csrf/index.js +2 -1
- package/plugins/dashboard/index.js +27 -7
- package/src/app-context.js +4 -188
- package/src/dev/typegen.js +1 -1
- package/src/file-router.js +12 -3
- package/src/plugin-manager.js +5 -861
- package/src/server.js +30 -34
- package/src/services/builtins/auth.js +2 -273
- package/src/services/builtins/exchange.js +2 -229
- package/src/services/builtins/file-manager.js +2 -178
- package/src/services/builtins/mail.js +2 -134
- package/src/services/builtins/media.js +2 -196
- package/src/services/builtins/system.js +2 -219
- package/src/services/discovery.js +2 -125
- package/src/services/executor.js +2 -362
- package/src/services/index.js +4 -60
- package/src/services/loader.js +2 -0
- package/src/services/memoize.js +2 -234
- package/src/services/registry.js +2 -314
- package/src/services/validator.js +2 -273
- package/core/utils/array.js +0 -12
- package/core/utils/base64url.js +0 -8
- package/core/utils/ensure-leading-slash.js +0 -6
- package/core/utils/escape-html.js +0 -7
- package/core/utils/escape-regexp.js +0 -7
- package/core/utils/get-client-ip.js +0 -6
- package/core/utils/hmac.js +0 -6
- package/core/utils/random.js +0 -9
- package/core/utils/safe-compare.js +0 -3
- package/core/utils/sha256.js +0 -6
- package/core/utils/strip-trailing-slash.js +0 -6
- 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
|
-
|
|
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('
|
|
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
|
package/core/mail/mailer.js
CHANGED
|
@@ -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(/<[
|
|
70
|
+
msg.text = msg.html.replace(/<[^<>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
try {
|
package/core/orm/eager-loader.js
CHANGED
|
@@ -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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
|
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
|
-
|
|
186
|
-
|
|
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
|
|
282
|
-
|
|
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
|
-
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
|
560
|
-
freeStaleMigrationLock
|
|
561
|
-
getMigrationStatus
|
|
562
|
-
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
|
};
|
package/core/orm/json-fields.js
CHANGED
|
@@ -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 (
|
|
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 (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
|