schemock 0.0.1
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/LICENSE +21 -0
- package/README.md +82 -0
- package/dist/adapters/index.d.mts +1364 -0
- package/dist/adapters/index.d.ts +1364 -0
- package/dist/adapters/index.js +36988 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/index.mjs +36972 -0
- package/dist/adapters/index.mjs.map +1 -0
- package/dist/cli/index.d.mts +831 -0
- package/dist/cli/index.d.ts +831 -0
- package/dist/cli/index.js +4425 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/index.mjs +4401 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/cli.js +6776 -0
- package/dist/index.d.mts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +39439 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +39367 -0
- package/dist/index.mjs.map +1 -0
- package/dist/middleware/index.d.mts +688 -0
- package/dist/middleware/index.d.ts +688 -0
- package/dist/middleware/index.js +921 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/index.mjs +899 -0
- package/dist/middleware/index.mjs.map +1 -0
- package/dist/react/index.d.mts +316 -0
- package/dist/react/index.d.ts +316 -0
- package/dist/react/index.js +466 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/index.mjs +456 -0
- package/dist/react/index.mjs.map +1 -0
- package/dist/runtime/index.d.mts +814 -0
- package/dist/runtime/index.d.ts +814 -0
- package/dist/runtime/index.js +1270 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/index.mjs +1246 -0
- package/dist/runtime/index.mjs.map +1 -0
- package/dist/schema/index.d.mts +838 -0
- package/dist/schema/index.d.ts +838 -0
- package/dist/schema/index.js +696 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/index.mjs +681 -0
- package/dist/schema/index.mjs.map +1 -0
- package/dist/types-C1MiZh1d.d.ts +96 -0
- package/dist/types-C2bd2vgy.d.mts +773 -0
- package/dist/types-C2bd2vgy.d.ts +773 -0
- package/dist/types-C9VMgu3E.d.mts +289 -0
- package/dist/types-DV2DS7wj.d.mts +96 -0
- package/dist/types-c2AN3vky.d.ts +289 -0
- package/package.json +116 -0
|
@@ -0,0 +1,1246 @@
|
|
|
1
|
+
import { http, HttpResponse } from 'msw';
|
|
2
|
+
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __esm = (fn, res) => function __init() {
|
|
6
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
7
|
+
};
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/runtime/handlers.ts
|
|
14
|
+
var handlers_exports = {};
|
|
15
|
+
__export(handlers_exports, {
|
|
16
|
+
createHandlers: () => createHandlers
|
|
17
|
+
});
|
|
18
|
+
function createHandlers(schemas, adapter, options) {
|
|
19
|
+
const handlers = [];
|
|
20
|
+
const baseUrl = options?.baseUrl ?? "/api";
|
|
21
|
+
for (const schema of schemas) {
|
|
22
|
+
const entityPath = getEntityPath(schema, baseUrl);
|
|
23
|
+
handlers.push(createListHandler(schema, entityPath, adapter));
|
|
24
|
+
handlers.push(createGetHandler(schema, entityPath, adapter));
|
|
25
|
+
handlers.push(createCreateHandler(schema, entityPath, adapter));
|
|
26
|
+
handlers.push(createUpdateHandler(schema, entityPath, adapter, "PUT"));
|
|
27
|
+
handlers.push(createUpdateHandler(schema, entityPath, adapter, "PATCH"));
|
|
28
|
+
handlers.push(createDeleteHandler(schema, entityPath, adapter));
|
|
29
|
+
if (schema.api?.operations) {
|
|
30
|
+
handlers.push(
|
|
31
|
+
...createCustomHandlers(schema, entityPath, adapter)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return handlers;
|
|
36
|
+
}
|
|
37
|
+
function getEntityPath(schema, baseUrl) {
|
|
38
|
+
if (schema.api?.basePath) {
|
|
39
|
+
return schema.api.basePath;
|
|
40
|
+
}
|
|
41
|
+
const plural = schema.name.endsWith("s") ? schema.name : `${schema.name}s`;
|
|
42
|
+
return `${baseUrl}/${plural}`;
|
|
43
|
+
}
|
|
44
|
+
function createListHandler(schema, path, adapter) {
|
|
45
|
+
return http.get(path, async ({ request }) => {
|
|
46
|
+
const url = new URL(request.url);
|
|
47
|
+
const ctx = {
|
|
48
|
+
entity: schema.name,
|
|
49
|
+
endpoint: path,
|
|
50
|
+
params: {},
|
|
51
|
+
filter: parseQueryFilter(url.searchParams),
|
|
52
|
+
limit: parseNumber(url.searchParams.get("limit")),
|
|
53
|
+
offset: parseNumber(url.searchParams.get("offset")),
|
|
54
|
+
orderBy: parseOrderBy(url.searchParams.get("orderBy"))
|
|
55
|
+
};
|
|
56
|
+
const result = await adapter.findMany(ctx);
|
|
57
|
+
if (result.error) {
|
|
58
|
+
return HttpResponse.json(
|
|
59
|
+
{ error: result.error.message },
|
|
60
|
+
{ status: 500 }
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return HttpResponse.json({
|
|
64
|
+
data: result.data,
|
|
65
|
+
meta: result.meta
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function createGetHandler(schema, path, adapter) {
|
|
70
|
+
return http.get(`${path}/:id`, async ({ params }) => {
|
|
71
|
+
const ctx = {
|
|
72
|
+
entity: schema.name,
|
|
73
|
+
endpoint: path,
|
|
74
|
+
params: { id: params.id }
|
|
75
|
+
};
|
|
76
|
+
const result = await adapter.findOne(ctx);
|
|
77
|
+
if (result.error) {
|
|
78
|
+
return HttpResponse.json(
|
|
79
|
+
{ error: result.error.message },
|
|
80
|
+
{ status: 404 }
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (!result.data) {
|
|
84
|
+
return HttpResponse.json(
|
|
85
|
+
{ error: `${schema.name} not found` },
|
|
86
|
+
{ status: 404 }
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return HttpResponse.json({ data: result.data });
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function createCreateHandler(schema, path, adapter) {
|
|
93
|
+
return http.post(path, async ({ request }) => {
|
|
94
|
+
const body = await request.json();
|
|
95
|
+
const ctx = {
|
|
96
|
+
entity: schema.name,
|
|
97
|
+
endpoint: path,
|
|
98
|
+
data: body
|
|
99
|
+
};
|
|
100
|
+
const result = await adapter.create(ctx);
|
|
101
|
+
if (result.error) {
|
|
102
|
+
return HttpResponse.json(
|
|
103
|
+
{ error: result.error.message },
|
|
104
|
+
{ status: 400 }
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return HttpResponse.json({ data: result.data }, { status: 201 });
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function createUpdateHandler(schema, path, adapter, method) {
|
|
111
|
+
const handler = method === "PUT" ? http.put : http.patch;
|
|
112
|
+
return handler(`${path}/:id`, async ({ params, request }) => {
|
|
113
|
+
const body = await request.json();
|
|
114
|
+
const ctx = {
|
|
115
|
+
entity: schema.name,
|
|
116
|
+
endpoint: path,
|
|
117
|
+
params: { id: params.id },
|
|
118
|
+
data: body
|
|
119
|
+
};
|
|
120
|
+
const result = await adapter.update(ctx);
|
|
121
|
+
if (result.error) {
|
|
122
|
+
return HttpResponse.json(
|
|
123
|
+
{ error: result.error.message },
|
|
124
|
+
{ status: 400 }
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (!result.data) {
|
|
128
|
+
return HttpResponse.json(
|
|
129
|
+
{ error: `${schema.name} not found` },
|
|
130
|
+
{ status: 404 }
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return HttpResponse.json({ data: result.data });
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function createDeleteHandler(schema, path, adapter) {
|
|
137
|
+
return http.delete(`${path}/:id`, async ({ params }) => {
|
|
138
|
+
const ctx = {
|
|
139
|
+
entity: schema.name,
|
|
140
|
+
endpoint: path,
|
|
141
|
+
params: { id: params.id }
|
|
142
|
+
};
|
|
143
|
+
const result = await adapter.delete(ctx);
|
|
144
|
+
if (result.error) {
|
|
145
|
+
return HttpResponse.json(
|
|
146
|
+
{ error: result.error.message },
|
|
147
|
+
{ status: 400 }
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return new HttpResponse(null, { status: 204 });
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function createCustomHandlers(schema, basePath, adapter) {
|
|
154
|
+
const handlers = [];
|
|
155
|
+
const operations = schema.api?.operations || {};
|
|
156
|
+
for (const [name, config] of Object.entries(operations)) {
|
|
157
|
+
if (typeof config === "boolean") continue;
|
|
158
|
+
if (!config || typeof config !== "object") continue;
|
|
159
|
+
const customConfig = config;
|
|
160
|
+
const method = customConfig.method?.toLowerCase() || "get";
|
|
161
|
+
const path = `${basePath}${customConfig.path || `/${name}`}`;
|
|
162
|
+
const httpMethod = http[method];
|
|
163
|
+
if (typeof httpMethod !== "function") continue;
|
|
164
|
+
handlers.push(
|
|
165
|
+
httpMethod(path, async ({ request, params }) => {
|
|
166
|
+
const ctx = {
|
|
167
|
+
entity: schema.name,
|
|
168
|
+
endpoint: path,
|
|
169
|
+
operation: name,
|
|
170
|
+
params
|
|
171
|
+
};
|
|
172
|
+
if (method === "post" || method === "put" || method === "patch") {
|
|
173
|
+
ctx.data = await request.json();
|
|
174
|
+
}
|
|
175
|
+
if (adapter.custom) {
|
|
176
|
+
const result = await adapter.custom(ctx);
|
|
177
|
+
if (result.error) {
|
|
178
|
+
return HttpResponse.json(
|
|
179
|
+
{ error: result.error.message },
|
|
180
|
+
{ status: 400 }
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return HttpResponse.json({ data: result.data });
|
|
184
|
+
}
|
|
185
|
+
return HttpResponse.json(
|
|
186
|
+
{ error: "Custom operations not supported by adapter" },
|
|
187
|
+
{ status: 501 }
|
|
188
|
+
);
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
return handlers;
|
|
193
|
+
}
|
|
194
|
+
function parseQueryFilter(params) {
|
|
195
|
+
const filter = {};
|
|
196
|
+
let hasFilter = false;
|
|
197
|
+
for (const [key, value] of params.entries()) {
|
|
198
|
+
if (["limit", "offset", "orderBy", "select"].includes(key)) continue;
|
|
199
|
+
if (key.startsWith("filter[") && key.endsWith("]")) {
|
|
200
|
+
const field = key.slice(7, -1);
|
|
201
|
+
filter[field] = parseValue(value);
|
|
202
|
+
hasFilter = true;
|
|
203
|
+
} else if (!key.includes("[")) {
|
|
204
|
+
filter[key] = parseValue(value);
|
|
205
|
+
hasFilter = true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return hasFilter ? filter : void 0;
|
|
209
|
+
}
|
|
210
|
+
function parseOrderBy(value) {
|
|
211
|
+
if (!value) return void 0;
|
|
212
|
+
const orderBy = {};
|
|
213
|
+
for (const part of value.split(",")) {
|
|
214
|
+
const [field, direction] = part.split(":");
|
|
215
|
+
if (field) {
|
|
216
|
+
orderBy[field] = direction === "desc" ? "desc" : "asc";
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return Object.keys(orderBy).length > 0 ? orderBy : void 0;
|
|
220
|
+
}
|
|
221
|
+
function parseNumber(value) {
|
|
222
|
+
if (!value) return void 0;
|
|
223
|
+
const num = parseInt(value, 10);
|
|
224
|
+
return isNaN(num) ? void 0 : num;
|
|
225
|
+
}
|
|
226
|
+
function parseValue(value) {
|
|
227
|
+
if (value === "true") return true;
|
|
228
|
+
if (value === "false") return false;
|
|
229
|
+
const num = Number(value);
|
|
230
|
+
if (!isNaN(num)) return num;
|
|
231
|
+
return value;
|
|
232
|
+
}
|
|
233
|
+
var init_handlers = __esm({
|
|
234
|
+
"src/runtime/handlers.ts"() {
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// src/runtime/resolver/registry.ts
|
|
239
|
+
var SchemaRegistry = class {
|
|
240
|
+
entities = /* @__PURE__ */ new Map();
|
|
241
|
+
views = /* @__PURE__ */ new Map();
|
|
242
|
+
/**
|
|
243
|
+
* Register an entity schema
|
|
244
|
+
* @param schema - The entity schema to register
|
|
245
|
+
*/
|
|
246
|
+
register(schema) {
|
|
247
|
+
if (this.entities.has(schema.name)) {
|
|
248
|
+
console.warn(`Schema '${schema.name}' is already registered. Overwriting.`);
|
|
249
|
+
}
|
|
250
|
+
this.entities.set(schema.name, schema);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Register a view schema
|
|
254
|
+
* @param view - The view schema to register
|
|
255
|
+
*/
|
|
256
|
+
registerView(view) {
|
|
257
|
+
if (this.views.has(view.name)) {
|
|
258
|
+
console.warn(`View '${view.name}' is already registered. Overwriting.`);
|
|
259
|
+
}
|
|
260
|
+
this.views.set(view.name, view);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Get an entity schema by name
|
|
264
|
+
* @param entityName - The entity name
|
|
265
|
+
* @returns EntitySchema or undefined if not found
|
|
266
|
+
*/
|
|
267
|
+
get(entityName) {
|
|
268
|
+
return this.entities.get(entityName);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Get an entity schema by name, throws if not found
|
|
272
|
+
* @param entityName - The entity name
|
|
273
|
+
* @returns EntitySchema
|
|
274
|
+
* @throws Error if entity not found
|
|
275
|
+
*/
|
|
276
|
+
getOrThrow(entityName) {
|
|
277
|
+
const schema = this.entities.get(entityName);
|
|
278
|
+
if (!schema) {
|
|
279
|
+
throw new Error(`Entity schema '${entityName}' not found. Did you forget to register it?`);
|
|
280
|
+
}
|
|
281
|
+
return schema;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Get a view schema by name
|
|
285
|
+
* @param viewName - The view name
|
|
286
|
+
* @returns ViewSchema or undefined if not found
|
|
287
|
+
*/
|
|
288
|
+
getView(viewName) {
|
|
289
|
+
return this.views.get(viewName);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Get a view schema by name, throws if not found
|
|
293
|
+
* @param viewName - The view name
|
|
294
|
+
* @returns ViewSchema
|
|
295
|
+
* @throws Error if view not found
|
|
296
|
+
*/
|
|
297
|
+
getViewOrThrow(viewName) {
|
|
298
|
+
const view = this.views.get(viewName);
|
|
299
|
+
if (!view) {
|
|
300
|
+
throw new Error(`View schema '${viewName}' not found. Did you forget to register it?`);
|
|
301
|
+
}
|
|
302
|
+
return view;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Get all registered entity schemas
|
|
306
|
+
* @returns Array of all entity schemas
|
|
307
|
+
*/
|
|
308
|
+
getAll() {
|
|
309
|
+
return Array.from(this.entities.values());
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Get all registered view schemas
|
|
313
|
+
* @returns Array of all view schemas
|
|
314
|
+
*/
|
|
315
|
+
getAllViews() {
|
|
316
|
+
return Array.from(this.views.values());
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Check if an entity schema is registered
|
|
320
|
+
* @param entityName - The entity name
|
|
321
|
+
* @returns true if registered
|
|
322
|
+
*/
|
|
323
|
+
has(entityName) {
|
|
324
|
+
return this.entities.has(entityName);
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Check if a view schema is registered
|
|
328
|
+
* @param viewName - The view name
|
|
329
|
+
* @returns true if registered
|
|
330
|
+
*/
|
|
331
|
+
hasView(viewName) {
|
|
332
|
+
return this.views.has(viewName);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Get all relations for an entity
|
|
336
|
+
* @param entityName - The entity name
|
|
337
|
+
* @returns Array of relation definitions with their names
|
|
338
|
+
*/
|
|
339
|
+
getRelationsFor(entityName) {
|
|
340
|
+
const schema = this.entities.get(entityName);
|
|
341
|
+
if (!schema || !schema.relations) {
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
344
|
+
return Object.entries(schema.relations).map(([name, relation]) => ({
|
|
345
|
+
name,
|
|
346
|
+
relation
|
|
347
|
+
}));
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Get all entities that have a relation to a target entity
|
|
351
|
+
* @param targetEntityName - The target entity name
|
|
352
|
+
* @returns Array of entity names that reference the target
|
|
353
|
+
*/
|
|
354
|
+
getEntitiesReferencingEntity(targetEntityName) {
|
|
355
|
+
const result = [];
|
|
356
|
+
for (const [entityName, schema] of this.entities) {
|
|
357
|
+
if (schema.relations) {
|
|
358
|
+
for (const relation of Object.values(schema.relations)) {
|
|
359
|
+
if (relation.target === targetEntityName) {
|
|
360
|
+
result.push(entityName);
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Get entity names in dependency order (topological sort)
|
|
370
|
+
* Entities with no dependencies come first
|
|
371
|
+
* @returns Array of entity names in order
|
|
372
|
+
*/
|
|
373
|
+
getEntityOrder() {
|
|
374
|
+
const visited = /* @__PURE__ */ new Set();
|
|
375
|
+
const result = [];
|
|
376
|
+
const visit = (entityName) => {
|
|
377
|
+
if (visited.has(entityName)) return;
|
|
378
|
+
visited.add(entityName);
|
|
379
|
+
const schema = this.entities.get(entityName);
|
|
380
|
+
if (schema?.relations) {
|
|
381
|
+
for (const relation of Object.values(schema.relations)) {
|
|
382
|
+
if (this.entities.has(relation.target)) {
|
|
383
|
+
visit(relation.target);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
result.push(entityName);
|
|
388
|
+
};
|
|
389
|
+
for (const entityName of this.entities.keys()) {
|
|
390
|
+
visit(entityName);
|
|
391
|
+
}
|
|
392
|
+
return result;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Clear all registered schemas
|
|
396
|
+
*/
|
|
397
|
+
clear() {
|
|
398
|
+
this.entities.clear();
|
|
399
|
+
this.views.clear();
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Get entity count
|
|
403
|
+
*/
|
|
404
|
+
get entityCount() {
|
|
405
|
+
return this.entities.size;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Get view count
|
|
409
|
+
*/
|
|
410
|
+
get viewCount() {
|
|
411
|
+
return this.views.size;
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
var registry = new SchemaRegistry();
|
|
415
|
+
|
|
416
|
+
// src/runtime/resolver/computed.ts
|
|
417
|
+
function topologicalSort(fields, allComputed) {
|
|
418
|
+
const result = [];
|
|
419
|
+
const visited = /* @__PURE__ */ new Set();
|
|
420
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
421
|
+
const fieldNames = new Set(fields.map(([name]) => name));
|
|
422
|
+
function visit(name) {
|
|
423
|
+
if (visited.has(name)) return;
|
|
424
|
+
if (visiting.has(name)) {
|
|
425
|
+
throw new Error(`Circular dependency detected in computed fields: ${name}`);
|
|
426
|
+
}
|
|
427
|
+
visiting.add(name);
|
|
428
|
+
const computed = allComputed[name];
|
|
429
|
+
if (computed?.dependsOn) {
|
|
430
|
+
for (const dep of computed.dependsOn) {
|
|
431
|
+
if (allComputed[dep] && fieldNames.has(dep)) {
|
|
432
|
+
visit(dep);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
visiting.delete(name);
|
|
437
|
+
visited.add(name);
|
|
438
|
+
const field = fields.find(([n]) => n === name);
|
|
439
|
+
if (field) {
|
|
440
|
+
result.push(field);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
for (const [name] of fields) {
|
|
444
|
+
visit(name);
|
|
445
|
+
}
|
|
446
|
+
return result;
|
|
447
|
+
}
|
|
448
|
+
var computeCache = /* @__PURE__ */ new Map();
|
|
449
|
+
function clearComputeCache() {
|
|
450
|
+
computeCache.clear();
|
|
451
|
+
}
|
|
452
|
+
function resolveComputedField(entity, fieldName, computed, db, context) {
|
|
453
|
+
const entityId = entity.id;
|
|
454
|
+
const cacheKey = `${entityId}:${fieldName}`;
|
|
455
|
+
if (computeCache.has(cacheKey)) {
|
|
456
|
+
return computeCache.get(cacheKey);
|
|
457
|
+
}
|
|
458
|
+
let value;
|
|
459
|
+
if (context.mode === "seed" && computed.mock) {
|
|
460
|
+
value = computed.mock();
|
|
461
|
+
} else {
|
|
462
|
+
value = computed.resolve(entity, db, context);
|
|
463
|
+
}
|
|
464
|
+
if (value instanceof Promise) {
|
|
465
|
+
return value.then((resolved) => {
|
|
466
|
+
computeCache.set(cacheKey, resolved);
|
|
467
|
+
return resolved;
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
computeCache.set(cacheKey, value);
|
|
471
|
+
return value;
|
|
472
|
+
}
|
|
473
|
+
async function resolveComputedFields(entity, schema, db, context) {
|
|
474
|
+
if (!schema.computed || Object.keys(schema.computed).length === 0) {
|
|
475
|
+
return entity;
|
|
476
|
+
}
|
|
477
|
+
const computedFields = Object.entries(schema.computed);
|
|
478
|
+
const sorted = topologicalSort(computedFields, schema.computed);
|
|
479
|
+
for (const [fieldName, computed] of sorted) {
|
|
480
|
+
const value = await resolveComputedField(entity, fieldName, computed, db, context);
|
|
481
|
+
entity[fieldName] = value;
|
|
482
|
+
}
|
|
483
|
+
return entity;
|
|
484
|
+
}
|
|
485
|
+
function resolveComputedFieldsSync(entity, schema, context) {
|
|
486
|
+
if (!schema.computed || Object.keys(schema.computed).length === 0) {
|
|
487
|
+
return entity;
|
|
488
|
+
}
|
|
489
|
+
const computedFields = Object.entries(schema.computed);
|
|
490
|
+
const sorted = topologicalSort(computedFields, schema.computed);
|
|
491
|
+
for (const [fieldName, computed] of sorted) {
|
|
492
|
+
if (computed.mock) {
|
|
493
|
+
entity[fieldName] = computed.mock();
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return entity;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/runtime/resolver/relation.ts
|
|
500
|
+
var DEFAULT_MAX_DEPTH = 3;
|
|
501
|
+
async function resolveRelation(entity, relationName, relation, db, registry2, options = {}, currentDepth = 0) {
|
|
502
|
+
const maxDepth = options.depth ?? DEFAULT_MAX_DEPTH;
|
|
503
|
+
if (currentDepth >= maxDepth) {
|
|
504
|
+
return relation.type === "hasMany" ? [] : null;
|
|
505
|
+
}
|
|
506
|
+
switch (relation.type) {
|
|
507
|
+
case "hasOne":
|
|
508
|
+
return resolveHasOne(entity, relation, db, registry2, options, currentDepth);
|
|
509
|
+
case "hasMany":
|
|
510
|
+
return resolveHasMany(entity, relation, db, registry2, options, currentDepth);
|
|
511
|
+
case "belongsTo":
|
|
512
|
+
return resolveBelongsTo(entity, relation, db, registry2, options, currentDepth);
|
|
513
|
+
default:
|
|
514
|
+
throw new Error(`Unknown relation type: ${relation.type}`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
async function resolveHasOne(entity, relation, db, registry2, options, currentDepth) {
|
|
518
|
+
const targetDb = db[relation.target];
|
|
519
|
+
if (!targetDb) {
|
|
520
|
+
throw new Error(`Target entity '${relation.target}' not found in database`);
|
|
521
|
+
}
|
|
522
|
+
const foreignKey = relation.foreignKey || `${entity.constructor.name.toLowerCase()}Id`;
|
|
523
|
+
const entityId = entity.id;
|
|
524
|
+
const related = targetDb.findFirst({
|
|
525
|
+
where: {
|
|
526
|
+
[foreignKey]: { equals: entityId }
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
if (!related) return null;
|
|
530
|
+
const targetSchema = registry2.get(relation.target);
|
|
531
|
+
if (targetSchema) {
|
|
532
|
+
await resolveNestedRelations(
|
|
533
|
+
related,
|
|
534
|
+
targetSchema,
|
|
535
|
+
db,
|
|
536
|
+
registry2,
|
|
537
|
+
options,
|
|
538
|
+
currentDepth + 1
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
return related;
|
|
542
|
+
}
|
|
543
|
+
async function resolveHasMany(entity, relation, db, registry2, options, currentDepth) {
|
|
544
|
+
const targetDb = db[relation.target];
|
|
545
|
+
if (!targetDb) {
|
|
546
|
+
throw new Error(`Target entity '${relation.target}' not found in database`);
|
|
547
|
+
}
|
|
548
|
+
const foreignKey = relation.foreignKey || `${entity.constructor.name.toLowerCase()}Id`;
|
|
549
|
+
const entityId = entity.id;
|
|
550
|
+
const query = {
|
|
551
|
+
where: {
|
|
552
|
+
[foreignKey]: { equals: entityId }
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
const limit = options.limit ?? relation.limit;
|
|
556
|
+
if (limit) {
|
|
557
|
+
query.take = limit;
|
|
558
|
+
}
|
|
559
|
+
const orderBy = options.orderBy ?? relation.orderBy;
|
|
560
|
+
if (orderBy) {
|
|
561
|
+
query.orderBy = orderBy;
|
|
562
|
+
}
|
|
563
|
+
const results = targetDb.findMany(query);
|
|
564
|
+
const targetSchema = registry2.get(relation.target);
|
|
565
|
+
if (targetSchema) {
|
|
566
|
+
for (const item of results) {
|
|
567
|
+
await resolveNestedRelations(
|
|
568
|
+
item,
|
|
569
|
+
targetSchema,
|
|
570
|
+
db,
|
|
571
|
+
registry2,
|
|
572
|
+
options,
|
|
573
|
+
currentDepth + 1
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return results;
|
|
578
|
+
}
|
|
579
|
+
async function resolveBelongsTo(entity, relation, db, registry2, options, currentDepth) {
|
|
580
|
+
const foreignKey = relation.foreignKey || `${relation.target}Id`;
|
|
581
|
+
const foreignKeyValue = entity[foreignKey];
|
|
582
|
+
if (!foreignKeyValue) return null;
|
|
583
|
+
const targetDb = db[relation.target];
|
|
584
|
+
if (!targetDb) {
|
|
585
|
+
throw new Error(`Target entity '${relation.target}' not found in database`);
|
|
586
|
+
}
|
|
587
|
+
const related = targetDb.findFirst({
|
|
588
|
+
where: { id: { equals: foreignKeyValue } }
|
|
589
|
+
});
|
|
590
|
+
if (!related) return null;
|
|
591
|
+
const targetSchema = registry2.get(relation.target);
|
|
592
|
+
if (targetSchema) {
|
|
593
|
+
await resolveNestedRelations(
|
|
594
|
+
related,
|
|
595
|
+
targetSchema,
|
|
596
|
+
db,
|
|
597
|
+
registry2,
|
|
598
|
+
options,
|
|
599
|
+
currentDepth + 1
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
return related;
|
|
603
|
+
}
|
|
604
|
+
async function resolveNestedRelations(entity, schema, db, registry2, options, currentDepth) {
|
|
605
|
+
if (!schema.relations) return;
|
|
606
|
+
const include = options.include ?? [];
|
|
607
|
+
for (const [relationName, relation] of Object.entries(schema.relations)) {
|
|
608
|
+
const shouldLoad = relation.eager || include.includes(relationName) || include.some((inc) => inc.startsWith(`${relationName}.`));
|
|
609
|
+
if (shouldLoad) {
|
|
610
|
+
const nestedIncludes = include.filter((inc) => inc.startsWith(`${relationName}.`)).map((inc) => inc.replace(`${relationName}.`, ""));
|
|
611
|
+
entity[relationName] = await resolveRelation(
|
|
612
|
+
entity,
|
|
613
|
+
relationName,
|
|
614
|
+
relation,
|
|
615
|
+
db,
|
|
616
|
+
registry2,
|
|
617
|
+
{ ...options, include: nestedIncludes },
|
|
618
|
+
currentDepth
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
async function resolveRelations(entity, schema, db, registry2, options = {}) {
|
|
624
|
+
await resolveNestedRelations(entity, schema, db, registry2, options, 0);
|
|
625
|
+
return entity;
|
|
626
|
+
}
|
|
627
|
+
async function eagerLoadRelations(entities, schema, db, registry2, options = {}) {
|
|
628
|
+
if (!schema.relations || entities.length === 0) {
|
|
629
|
+
return entities;
|
|
630
|
+
}
|
|
631
|
+
const include = options.include ?? [];
|
|
632
|
+
for (const [relationName, relation] of Object.entries(schema.relations)) {
|
|
633
|
+
const shouldLoad = relation.eager || include.includes(relationName) || include.some((inc) => inc.startsWith(`${relationName}.`));
|
|
634
|
+
if (!shouldLoad) continue;
|
|
635
|
+
if (relation.type === "belongsTo") {
|
|
636
|
+
await batchLoadBelongsTo(entities, relationName, relation, db, registry2, options);
|
|
637
|
+
} else if (relation.type === "hasOne") {
|
|
638
|
+
await batchLoadHasOne(entities, relationName, relation, db, registry2, options);
|
|
639
|
+
} else if (relation.type === "hasMany") {
|
|
640
|
+
for (const entity of entities) {
|
|
641
|
+
entity[relationName] = await resolveRelation(entity, relationName, relation, db, registry2, options);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return entities;
|
|
646
|
+
}
|
|
647
|
+
async function batchLoadBelongsTo(entities, relationName, relation, db, registry2, options) {
|
|
648
|
+
const foreignKey = relation.foreignKey || `${relation.target}Id`;
|
|
649
|
+
const targetDb = db[relation.target];
|
|
650
|
+
const foreignKeyValues = [...new Set(entities.map((e) => e[foreignKey]).filter(Boolean))];
|
|
651
|
+
if (foreignKeyValues.length === 0) {
|
|
652
|
+
entities.forEach((e) => e[relationName] = null);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const relatedEntities = targetDb.findMany({
|
|
656
|
+
where: {
|
|
657
|
+
id: { in: foreignKeyValues }
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
const lookup = new Map(relatedEntities.map((r) => [r.id, r]));
|
|
661
|
+
for (const entity of entities) {
|
|
662
|
+
const fkValue = entity[foreignKey];
|
|
663
|
+
entity[relationName] = fkValue ? lookup.get(fkValue) ?? null : null;
|
|
664
|
+
}
|
|
665
|
+
const targetSchema = registry2.get(relation.target);
|
|
666
|
+
if (targetSchema) {
|
|
667
|
+
const nestedIncludes = (options.include ?? []).filter((inc) => inc.startsWith(`${relationName}.`)).map((inc) => inc.replace(`${relationName}.`, ""));
|
|
668
|
+
if (nestedIncludes.length > 0) {
|
|
669
|
+
await eagerLoadRelations(relatedEntities, targetSchema, db, registry2, {
|
|
670
|
+
...options,
|
|
671
|
+
include: nestedIncludes
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
async function batchLoadHasOne(entities, relationName, relation, db, registry2, options) {
|
|
677
|
+
const foreignKey = relation.foreignKey || "id";
|
|
678
|
+
const targetDb = db[relation.target];
|
|
679
|
+
const entityIds = entities.map((e) => e.id).filter(Boolean);
|
|
680
|
+
if (entityIds.length === 0) {
|
|
681
|
+
entities.forEach((e) => e[relationName] = null);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const relatedEntities = targetDb.findMany({
|
|
685
|
+
where: {
|
|
686
|
+
[foreignKey]: { in: entityIds }
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
const lookup = new Map(relatedEntities.map((r) => [r[foreignKey], r]));
|
|
690
|
+
for (const entity of entities) {
|
|
691
|
+
entity[relationName] = lookup.get(entity.id) ?? null;
|
|
692
|
+
}
|
|
693
|
+
const targetSchema = registry2.get(relation.target);
|
|
694
|
+
if (targetSchema) {
|
|
695
|
+
const nestedIncludes = (options.include ?? []).filter((inc) => inc.startsWith(`${relationName}.`)).map((inc) => inc.replace(`${relationName}.`, ""));
|
|
696
|
+
if (nestedIncludes.length > 0) {
|
|
697
|
+
await eagerLoadRelations(relatedEntities, targetSchema, db, registry2, {
|
|
698
|
+
...options,
|
|
699
|
+
include: nestedIncludes
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// src/schema/types.ts
|
|
706
|
+
function isComputedField(field) {
|
|
707
|
+
return typeof field === "object" && field !== null && "_computed" in field && field._computed === true;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/runtime/resolver/view.ts
|
|
711
|
+
function isEmbedDefinition(value) {
|
|
712
|
+
return typeof value === "object" && value !== null && "_embed" in value && value._embed === true;
|
|
713
|
+
}
|
|
714
|
+
var ViewResolver = class {
|
|
715
|
+
constructor(registry2, db) {
|
|
716
|
+
this.registry = registry2;
|
|
717
|
+
this.db = db;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Resolves a view schema with given parameters.
|
|
721
|
+
*
|
|
722
|
+
* @param view - The view schema to resolve
|
|
723
|
+
* @param params - URL parameters for the view
|
|
724
|
+
* @param context - Optional resolver context
|
|
725
|
+
* @returns The resolved view data
|
|
726
|
+
*
|
|
727
|
+
* @example
|
|
728
|
+
* ```typescript
|
|
729
|
+
* const userData = await viewResolver.resolve(UserFullView, { id: 'user-123' });
|
|
730
|
+
* ```
|
|
731
|
+
*/
|
|
732
|
+
async resolve(view, params, context) {
|
|
733
|
+
clearComputeCache();
|
|
734
|
+
const resolverContext = {
|
|
735
|
+
mode: "resolve",
|
|
736
|
+
params,
|
|
737
|
+
...context
|
|
738
|
+
};
|
|
739
|
+
return this.buildViewResult(view, { params, context: resolverContext });
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Builds the view result by resolving all fields
|
|
743
|
+
*/
|
|
744
|
+
async buildViewResult(view, options) {
|
|
745
|
+
const result = {};
|
|
746
|
+
for (const [fieldName, fieldDef] of Object.entries(view.fields)) {
|
|
747
|
+
if (isEmbedDefinition(fieldDef)) {
|
|
748
|
+
result[fieldName] = await this.resolveEmbed(fieldDef, result, options);
|
|
749
|
+
} else if (isComputedField(fieldDef)) {
|
|
750
|
+
result[fieldName] = await this.resolveViewComputedField(fieldDef, result, options);
|
|
751
|
+
} else if (this.isNestedObjectField(fieldDef)) {
|
|
752
|
+
result[fieldName] = await this.resolveNestedObject(fieldDef, result, options);
|
|
753
|
+
} else {
|
|
754
|
+
const paramValue = options.params[fieldName];
|
|
755
|
+
if (paramValue !== void 0) {
|
|
756
|
+
result[fieldName] = paramValue;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return result;
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Check if a field definition is a nested object
|
|
764
|
+
*/
|
|
765
|
+
isNestedObjectField(fieldDef) {
|
|
766
|
+
return typeof fieldDef === "object" && fieldDef !== null && !("type" in fieldDef) && !("_computed" in fieldDef) && !("_embed" in fieldDef);
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Resolves an embedded entity field
|
|
770
|
+
*/
|
|
771
|
+
async resolveEmbed(embed, parentData, options) {
|
|
772
|
+
const targetDb = this.db[embed.entity.name];
|
|
773
|
+
if (!targetDb) {
|
|
774
|
+
throw new Error(`Target entity '${embed.entity.name}' not found in database`);
|
|
775
|
+
}
|
|
776
|
+
const query = { where: {} };
|
|
777
|
+
const idParam = options.params.id;
|
|
778
|
+
if (idParam) {
|
|
779
|
+
const possibleForeignKeys = [
|
|
780
|
+
`${this.guessParentEntityName(options)}Id`,
|
|
781
|
+
"userId",
|
|
782
|
+
"authorId",
|
|
783
|
+
"parentId"
|
|
784
|
+
];
|
|
785
|
+
for (const fk of possibleForeignKeys) {
|
|
786
|
+
if (embed.entity.fields && fk in embed.entity.fields) {
|
|
787
|
+
query.where[fk] = { equals: idParam };
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (embed.config?.limit) {
|
|
793
|
+
query.take = embed.config.limit;
|
|
794
|
+
}
|
|
795
|
+
if (embed.config?.orderBy) {
|
|
796
|
+
query.orderBy = embed.config.orderBy;
|
|
797
|
+
}
|
|
798
|
+
const shouldReturnArray = !embed.config?.limit || embed.config.limit > 1;
|
|
799
|
+
if (shouldReturnArray) {
|
|
800
|
+
const results = targetDb.findMany(query);
|
|
801
|
+
return results;
|
|
802
|
+
} else {
|
|
803
|
+
const result = targetDb.findFirst(query);
|
|
804
|
+
return result;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Guess the parent entity name from the view endpoint
|
|
809
|
+
*/
|
|
810
|
+
guessParentEntityName(options) {
|
|
811
|
+
return "user";
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Resolves a computed field within a view context
|
|
815
|
+
*/
|
|
816
|
+
async resolveViewComputedField(computed, currentData, options) {
|
|
817
|
+
if (options.context.mode === "seed" && computed.mock) {
|
|
818
|
+
return computed.mock();
|
|
819
|
+
}
|
|
820
|
+
return computed.resolve(currentData, this.db, options.context);
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Resolves a nested object containing fields and computed values
|
|
824
|
+
*/
|
|
825
|
+
async resolveNestedObject(nestedDef, parentData, options) {
|
|
826
|
+
const result = {};
|
|
827
|
+
for (const [fieldName, fieldDef] of Object.entries(nestedDef)) {
|
|
828
|
+
if (isComputedField(fieldDef)) {
|
|
829
|
+
result[fieldName] = await this.resolveViewComputedField(fieldDef, parentData, options);
|
|
830
|
+
} else {
|
|
831
|
+
result[fieldName] = fieldDef;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return result;
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Resolves a view with mock data (for seeding)
|
|
838
|
+
*/
|
|
839
|
+
async resolveMock(view, params) {
|
|
840
|
+
return this.resolve(view, params, { mode: "seed" });
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
function createViewResolver(registry2, db) {
|
|
844
|
+
return new ViewResolver(registry2, db);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// src/runtime/resolver/resolver.ts
|
|
848
|
+
var Resolver = class _Resolver {
|
|
849
|
+
constructor(registry2, db, context) {
|
|
850
|
+
this.registry = registry2;
|
|
851
|
+
this.db = db;
|
|
852
|
+
this.context = { mode: "resolve", ...context };
|
|
853
|
+
this.viewResolver = new ViewResolver(registry2, db);
|
|
854
|
+
}
|
|
855
|
+
context;
|
|
856
|
+
viewResolver;
|
|
857
|
+
/**
|
|
858
|
+
* Find a single entity by ID
|
|
859
|
+
*
|
|
860
|
+
* @param entityName - The entity type to find
|
|
861
|
+
* @param id - The entity ID
|
|
862
|
+
* @param options - Query options (relations, computed fields)
|
|
863
|
+
* @returns The entity or null if not found
|
|
864
|
+
*
|
|
865
|
+
* @example
|
|
866
|
+
* ```typescript
|
|
867
|
+
* const user = await resolver.findOne('user', '123', {
|
|
868
|
+
* include: ['profile'],
|
|
869
|
+
* computed: ['postCount'],
|
|
870
|
+
* });
|
|
871
|
+
* ```
|
|
872
|
+
*/
|
|
873
|
+
async findOne(entityName, id, options = {}) {
|
|
874
|
+
clearComputeCache();
|
|
875
|
+
const schema = this.registry.getOrThrow(entityName);
|
|
876
|
+
const entityDb = this.db[entityName];
|
|
877
|
+
if (!entityDb) {
|
|
878
|
+
throw new Error(`Entity '${entityName}' not found in database`);
|
|
879
|
+
}
|
|
880
|
+
const entity = entityDb.findFirst({
|
|
881
|
+
where: { id: { equals: id } }
|
|
882
|
+
});
|
|
883
|
+
if (!entity) return null;
|
|
884
|
+
await resolveRelations(entity, schema, this.db, this.registry, options);
|
|
885
|
+
await resolveComputedFields(entity, schema, this.db, this.context);
|
|
886
|
+
return entity;
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Find multiple entities with filtering and pagination
|
|
890
|
+
*
|
|
891
|
+
* @param entityName - The entity type to find
|
|
892
|
+
* @param options - Query options (filters, pagination, relations)
|
|
893
|
+
* @returns Array of entities
|
|
894
|
+
*
|
|
895
|
+
* @example
|
|
896
|
+
* ```typescript
|
|
897
|
+
* const users = await resolver.findMany('user', {
|
|
898
|
+
* where: { role: 'admin' },
|
|
899
|
+
* limit: 20,
|
|
900
|
+
* offset: 0,
|
|
901
|
+
* include: ['profile'],
|
|
902
|
+
* });
|
|
903
|
+
* ```
|
|
904
|
+
*/
|
|
905
|
+
async findMany(entityName, options = {}) {
|
|
906
|
+
clearComputeCache();
|
|
907
|
+
const schema = this.registry.getOrThrow(entityName);
|
|
908
|
+
const entityDb = this.db[entityName];
|
|
909
|
+
if (!entityDb) {
|
|
910
|
+
throw new Error(`Entity '${entityName}' not found in database`);
|
|
911
|
+
}
|
|
912
|
+
const query = {};
|
|
913
|
+
if (options.where) {
|
|
914
|
+
query.where = this.buildWhereClause(options.where);
|
|
915
|
+
}
|
|
916
|
+
if (options.limit) {
|
|
917
|
+
query.take = options.limit;
|
|
918
|
+
}
|
|
919
|
+
if (options.offset) {
|
|
920
|
+
query.skip = options.offset;
|
|
921
|
+
}
|
|
922
|
+
if (options.orderBy) {
|
|
923
|
+
query.orderBy = options.orderBy;
|
|
924
|
+
}
|
|
925
|
+
const entities = entityDb.findMany(query);
|
|
926
|
+
await eagerLoadRelations(entities, schema, this.db, this.registry, options);
|
|
927
|
+
for (const entity of entities) {
|
|
928
|
+
await resolveComputedFields(entity, schema, this.db, this.context);
|
|
929
|
+
}
|
|
930
|
+
return entities;
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Create a new entity
|
|
934
|
+
*
|
|
935
|
+
* @param entityName - The entity type to create
|
|
936
|
+
* @param data - The entity data
|
|
937
|
+
* @param options - Query options for the returned entity
|
|
938
|
+
* @returns The created entity with relations and computed fields
|
|
939
|
+
*
|
|
940
|
+
* @example
|
|
941
|
+
* ```typescript
|
|
942
|
+
* const user = await resolver.create('user', {
|
|
943
|
+
* name: 'John Doe',
|
|
944
|
+
* email: 'john@example.com',
|
|
945
|
+
* });
|
|
946
|
+
* ```
|
|
947
|
+
*/
|
|
948
|
+
async create(entityName, data, options = {}) {
|
|
949
|
+
const schema = this.registry.getOrThrow(entityName);
|
|
950
|
+
const entityDb = this.db[entityName];
|
|
951
|
+
if (!entityDb) {
|
|
952
|
+
throw new Error(`Entity '${entityName}' not found in database`);
|
|
953
|
+
}
|
|
954
|
+
if (!entityDb.create) {
|
|
955
|
+
throw new Error(`Entity '${entityName}' does not support create operation`);
|
|
956
|
+
}
|
|
957
|
+
const entity = entityDb.create(data);
|
|
958
|
+
await resolveRelations(entity, schema, this.db, this.registry, options);
|
|
959
|
+
await resolveComputedFields(entity, schema, this.db, this.context);
|
|
960
|
+
return entity;
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Update an existing entity
|
|
964
|
+
*
|
|
965
|
+
* @param entityName - The entity type to update
|
|
966
|
+
* @param id - The entity ID
|
|
967
|
+
* @param data - The update data
|
|
968
|
+
* @param options - Query options for the returned entity
|
|
969
|
+
* @returns The updated entity or null if not found
|
|
970
|
+
*
|
|
971
|
+
* @example
|
|
972
|
+
* ```typescript
|
|
973
|
+
* const user = await resolver.update('user', '123', {
|
|
974
|
+
* name: 'Jane Doe',
|
|
975
|
+
* });
|
|
976
|
+
* ```
|
|
977
|
+
*/
|
|
978
|
+
async update(entityName, id, data, options = {}) {
|
|
979
|
+
const schema = this.registry.getOrThrow(entityName);
|
|
980
|
+
const entityDb = this.db[entityName];
|
|
981
|
+
if (!entityDb) {
|
|
982
|
+
throw new Error(`Entity '${entityName}' not found in database`);
|
|
983
|
+
}
|
|
984
|
+
if (!entityDb.update) {
|
|
985
|
+
throw new Error(`Entity '${entityName}' does not support update operation`);
|
|
986
|
+
}
|
|
987
|
+
const entity = entityDb.update({
|
|
988
|
+
where: { id: { equals: id } },
|
|
989
|
+
data
|
|
990
|
+
});
|
|
991
|
+
if (!entity) return null;
|
|
992
|
+
await resolveRelations(entity, schema, this.db, this.registry, options);
|
|
993
|
+
await resolveComputedFields(entity, schema, this.db, this.context);
|
|
994
|
+
return entity;
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Delete an entity by ID
|
|
998
|
+
*
|
|
999
|
+
* @param entityName - The entity type to delete
|
|
1000
|
+
* @param id - The entity ID
|
|
1001
|
+
* @returns true if deleted, false if not found
|
|
1002
|
+
*
|
|
1003
|
+
* @example
|
|
1004
|
+
* ```typescript
|
|
1005
|
+
* const deleted = await resolver.delete('user', '123');
|
|
1006
|
+
* ```
|
|
1007
|
+
*/
|
|
1008
|
+
delete(entityName, id) {
|
|
1009
|
+
const entityDb = this.db[entityName];
|
|
1010
|
+
if (!entityDb) {
|
|
1011
|
+
throw new Error(`Entity '${entityName}' not found in database`);
|
|
1012
|
+
}
|
|
1013
|
+
if (!entityDb.delete) {
|
|
1014
|
+
throw new Error(`Entity '${entityName}' does not support delete operation`);
|
|
1015
|
+
}
|
|
1016
|
+
const deleted = entityDb.delete({
|
|
1017
|
+
where: { id: { equals: id } }
|
|
1018
|
+
});
|
|
1019
|
+
return deleted !== null;
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Count entities matching a filter
|
|
1023
|
+
*
|
|
1024
|
+
* @param entityName - The entity type to count
|
|
1025
|
+
* @param options - Count options (filters)
|
|
1026
|
+
* @returns The count
|
|
1027
|
+
*
|
|
1028
|
+
* @example
|
|
1029
|
+
* ```typescript
|
|
1030
|
+
* const adminCount = await resolver.count('user', {
|
|
1031
|
+
* where: { role: 'admin' },
|
|
1032
|
+
* });
|
|
1033
|
+
* ```
|
|
1034
|
+
*/
|
|
1035
|
+
count(entityName, options = {}) {
|
|
1036
|
+
const entityDb = this.db[entityName];
|
|
1037
|
+
if (!entityDb) {
|
|
1038
|
+
throw new Error(`Entity '${entityName}' not found in database`);
|
|
1039
|
+
}
|
|
1040
|
+
const query = options.where ? { where: this.buildWhereClause(options.where) } : {};
|
|
1041
|
+
return entityDb.count(query);
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Resolve a view with parameters
|
|
1045
|
+
*
|
|
1046
|
+
* @param viewName - The view name
|
|
1047
|
+
* @param params - View parameters
|
|
1048
|
+
* @returns The resolved view data
|
|
1049
|
+
*
|
|
1050
|
+
* @example
|
|
1051
|
+
* ```typescript
|
|
1052
|
+
* const userFull = await resolver.view('user-full', { id: '123' });
|
|
1053
|
+
* ```
|
|
1054
|
+
*/
|
|
1055
|
+
async view(viewName, params) {
|
|
1056
|
+
clearComputeCache();
|
|
1057
|
+
const viewSchema = this.registry.getViewOrThrow(viewName);
|
|
1058
|
+
return this.viewResolver.resolve(viewSchema, params, this.context);
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Build a where clause from simple object to database format
|
|
1062
|
+
*/
|
|
1063
|
+
buildWhereClause(where) {
|
|
1064
|
+
const clause = {};
|
|
1065
|
+
for (const [key, value] of Object.entries(where)) {
|
|
1066
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
1067
|
+
clause[key] = value;
|
|
1068
|
+
} else if (Array.isArray(value)) {
|
|
1069
|
+
clause[key] = { in: value };
|
|
1070
|
+
} else {
|
|
1071
|
+
clause[key] = { equals: value };
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return clause;
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Get the current resolver context
|
|
1078
|
+
*/
|
|
1079
|
+
getContext() {
|
|
1080
|
+
return { ...this.context };
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Create a new resolver with updated context
|
|
1084
|
+
*/
|
|
1085
|
+
withContext(context) {
|
|
1086
|
+
return new _Resolver(this.registry, this.db, { ...this.context, ...context });
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
function createResolver(registry2, db, context) {
|
|
1090
|
+
return new Resolver(registry2, db, context);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// src/runtime/setup.ts
|
|
1094
|
+
var state = {
|
|
1095
|
+
initialized: false,
|
|
1096
|
+
adapter: null,
|
|
1097
|
+
schemas: [],
|
|
1098
|
+
worker: null,
|
|
1099
|
+
server: null
|
|
1100
|
+
};
|
|
1101
|
+
async function setup(options) {
|
|
1102
|
+
if (state.initialized) {
|
|
1103
|
+
console.warn("Schemock runtime already initialized");
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
if (options) {
|
|
1107
|
+
state.adapter = options.adapter;
|
|
1108
|
+
state.schemas = options.schemas;
|
|
1109
|
+
}
|
|
1110
|
+
if (options?.startMsw && typeof window !== "undefined" && options.schemas.length > 0) {
|
|
1111
|
+
await startMswWorker(options);
|
|
1112
|
+
}
|
|
1113
|
+
state.initialized = true;
|
|
1114
|
+
}
|
|
1115
|
+
async function startMswWorker(options) {
|
|
1116
|
+
try {
|
|
1117
|
+
const { setupWorker } = await import('msw/browser');
|
|
1118
|
+
const { createHandlers: createHandlers2 } = await Promise.resolve().then(() => (init_handlers(), handlers_exports));
|
|
1119
|
+
const handlers = createHandlers2(
|
|
1120
|
+
options.schemas,
|
|
1121
|
+
state.adapter,
|
|
1122
|
+
options.mswOptions
|
|
1123
|
+
);
|
|
1124
|
+
state.worker = setupWorker(...handlers);
|
|
1125
|
+
await state.worker.start({
|
|
1126
|
+
quiet: options.mswOptions?.quiet ?? true
|
|
1127
|
+
});
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
console.warn("MSW setup failed:", error);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
async function teardown() {
|
|
1133
|
+
if (state.worker) {
|
|
1134
|
+
await state.worker.stop();
|
|
1135
|
+
state.worker = null;
|
|
1136
|
+
}
|
|
1137
|
+
state.adapter = null;
|
|
1138
|
+
state.schemas = [];
|
|
1139
|
+
state.initialized = false;
|
|
1140
|
+
}
|
|
1141
|
+
function isInitialized() {
|
|
1142
|
+
return state.initialized;
|
|
1143
|
+
}
|
|
1144
|
+
function getAdapter() {
|
|
1145
|
+
return state.adapter;
|
|
1146
|
+
}
|
|
1147
|
+
function setAdapter(adapter) {
|
|
1148
|
+
state.adapter = adapter;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// src/runtime/seed.ts
|
|
1152
|
+
function seed(counts) {
|
|
1153
|
+
const adapter = getAdapter();
|
|
1154
|
+
if (!adapter) {
|
|
1155
|
+
throw new Error(
|
|
1156
|
+
"No adapter configured. Call setup() first or use adapter.seed() directly."
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
if (adapter.name !== "mock") {
|
|
1160
|
+
throw new Error(
|
|
1161
|
+
`seed() only works with MockAdapter. Current adapter: ${adapter.name}`
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
adapter.seed(counts);
|
|
1165
|
+
}
|
|
1166
|
+
function reset() {
|
|
1167
|
+
const adapter = getAdapter();
|
|
1168
|
+
if (!adapter) {
|
|
1169
|
+
throw new Error("No adapter configured. Call setup() first.");
|
|
1170
|
+
}
|
|
1171
|
+
if (adapter.name !== "mock") {
|
|
1172
|
+
throw new Error(
|
|
1173
|
+
`reset() only works with MockAdapter. Current adapter: ${adapter.name}`
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
adapter.reset();
|
|
1177
|
+
}
|
|
1178
|
+
function seedWithRelations(counts, relations) {
|
|
1179
|
+
const adapter = getAdapter();
|
|
1180
|
+
if (!adapter || adapter.name !== "mock") {
|
|
1181
|
+
throw new Error("seedWithRelations() requires MockAdapter");
|
|
1182
|
+
}
|
|
1183
|
+
const deps = /* @__PURE__ */ new Map();
|
|
1184
|
+
for (const entity of Object.keys(counts)) {
|
|
1185
|
+
deps.set(entity, /* @__PURE__ */ new Set());
|
|
1186
|
+
}
|
|
1187
|
+
for (const [entity, fks] of Object.entries(relations)) {
|
|
1188
|
+
for (const targetEntity of Object.values(fks)) {
|
|
1189
|
+
deps.get(entity)?.add(targetEntity);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
const sorted = [];
|
|
1193
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1194
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
1195
|
+
function visit(entity) {
|
|
1196
|
+
if (visited.has(entity)) return;
|
|
1197
|
+
if (visiting.has(entity)) {
|
|
1198
|
+
throw new Error(`Circular dependency detected: ${entity}`);
|
|
1199
|
+
}
|
|
1200
|
+
visiting.add(entity);
|
|
1201
|
+
for (const dep of deps.get(entity) || []) {
|
|
1202
|
+
visit(dep);
|
|
1203
|
+
}
|
|
1204
|
+
visiting.delete(entity);
|
|
1205
|
+
visited.add(entity);
|
|
1206
|
+
sorted.push(entity);
|
|
1207
|
+
}
|
|
1208
|
+
for (const entity of Object.keys(counts)) {
|
|
1209
|
+
visit(entity);
|
|
1210
|
+
}
|
|
1211
|
+
const entityIds = {};
|
|
1212
|
+
for (const entity of sorted) {
|
|
1213
|
+
const count = counts[entity];
|
|
1214
|
+
if (count === void 0) continue;
|
|
1215
|
+
const fks = relations[entity] || {};
|
|
1216
|
+
const parentIds = {};
|
|
1217
|
+
for (const [fkField, parentEntity] of Object.entries(fks)) {
|
|
1218
|
+
parentIds[fkField] = entityIds[parentEntity] || [];
|
|
1219
|
+
}
|
|
1220
|
+
entityIds[entity] = [];
|
|
1221
|
+
for (let i = 0; i < count; i++) {
|
|
1222
|
+
const data = {};
|
|
1223
|
+
for (const [fkField, ids] of Object.entries(parentIds)) {
|
|
1224
|
+
if (ids.length > 0) {
|
|
1225
|
+
data[fkField] = ids[Math.floor(Math.random() * ids.length)];
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
const result = adapter.create({
|
|
1229
|
+
entity,
|
|
1230
|
+
data
|
|
1231
|
+
});
|
|
1232
|
+
result.then((res) => {
|
|
1233
|
+
if (res.data?.id) {
|
|
1234
|
+
entityIds[entity].push(res.data.id);
|
|
1235
|
+
}
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// src/runtime/index.ts
|
|
1242
|
+
init_handlers();
|
|
1243
|
+
|
|
1244
|
+
export { Resolver, SchemaRegistry, ViewResolver, clearComputeCache, createHandlers, createResolver, createViewResolver, eagerLoadRelations, getAdapter, isInitialized, registry, reset, resolveComputedField, resolveComputedFields, resolveComputedFieldsSync, resolveRelation, resolveRelations, seed, seedWithRelations, setAdapter, setup, teardown, topologicalSort };
|
|
1245
|
+
//# sourceMappingURL=index.mjs.map
|
|
1246
|
+
//# sourceMappingURL=index.mjs.map
|