cloesce 0.0.3-fix.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.
@@ -0,0 +1,108 @@
1
+ import { D1Database } from "@cloudflare/workers-types/experimental/index.js";
2
+ import { HttpResult, Either, ModelMethod, CloesceAst, Model } from "./common.js";
3
+ import { IncludeTree } from "./index.js";
4
+ /**
5
+ * A map of model names to their respective constructor.
6
+ *
7
+ * The value accepted into the `cloesce` function is generated by the Cloesce compiler, and
8
+ * is guaranteed to contain all model definitions.
9
+ */
10
+ type ModelConstructorRegistry = Record<string, new () => UserDefinedModel>;
11
+ /**
12
+ * A dependency injection container, mapping an object type name to an instance of that object.
13
+ *
14
+ * The value accepted into the `cloesce` function is generated by the Cloesce compiler, and is
15
+ * guaranteed to contain all injected model method parameters.
16
+ */
17
+ type InstanceRegistry = Map<string, any>;
18
+ /**
19
+ * Users will create Cloesce models, which have metadata for them in the ast.
20
+ * For TypeScript's purposes, these models can be anything. We can assume any
21
+ * `UserDefinedModel` has been verified by the compiler.
22
+ */
23
+ type UserDefinedModel = any;
24
+ type InstantiatedUserDefinedModel = object;
25
+ /**
26
+ * Given a request, this represents a map of each body / url param name to
27
+ * its actual value. Unknown, as the a request can be anything.
28
+ */
29
+ type RequestParamMap = Record<string, unknown>;
30
+ /**
31
+ * Meta information on the wrangler env and db bindings
32
+ */
33
+ interface MetaWranglerEnv {
34
+ envName: string;
35
+ dbName: string;
36
+ }
37
+ /**
38
+ * Creates model instances given a properly formatted SQL record
39
+ * (either a foreign-key-less model or derived from a Cloesce generated view)
40
+ * @param ctor The type of the model
41
+ * @param records SQL records
42
+ * @param includeTree The include tree to use when parsing the records
43
+ * @returns
44
+ */
45
+ export declare function modelsFromSql<T>(ctor: new () => T, records: Record<string, any>[], includeTree: IncludeTree<T> | null): T[];
46
+ /**
47
+ * Cloesce entry point. Given a request, undergoes routing, validating,
48
+ * hydrating, and method dispatch.
49
+ * @param ast The CIDL AST
50
+ * @param constructorRegistry A mapping of user defined class names to their respective constructor
51
+ * @param instanceRegistry A mapping of a dependency class name to its instantiated object.
52
+ * @param request An incoming request to the workers server
53
+ * @param api_route The url's path to the api, e.g. api/v1/fooapi/
54
+ * @param envMeta Meta information on the wrangler env and D1 databases
55
+ * @returns A Response with an `HttpResult` JSON body.
56
+ */
57
+ export declare function cloesce(request: Request, ast: CloesceAst, constructorRegistry: ModelConstructorRegistry, instanceRegistry: InstanceRegistry, envMeta: MetaWranglerEnv, api_route: string): Promise<Response>;
58
+ /**
59
+ * Matches a request to a method on a model.
60
+ * @param api_route The route from the domain to the actual API, ie https://foo.com/route/to/api => route/to/api/
61
+ * @returns 404 or a `MatchedRoute`
62
+ */
63
+ declare function matchRoute(request: Request, ast: CloesceAst, api_route: string): Either<HttpResult, MatchedRoute>;
64
+ /**
65
+ * Validates the request's body/search params against a ModelMethod
66
+ * @returns 400 or a `RequestParamMap` consisting of each parameters name mapped to its value, and
67
+ * a data source
68
+ */
69
+ declare function validateRequest(request: Request, ast: CloesceAst, model: Model, method: ModelMethod, id: string | null): Promise<Either<HttpResult, [RequestParamMap, string | null]>>;
70
+ /**
71
+ * Queries D1 for a particular model's ID, then transforms the SQL column output into
72
+ * an instance of a model using the provided include tree and metadata as a guide.
73
+ * @returns 404 if no record was found for the provided ID
74
+ * @returns 500 if the D1 database is not synced with Cloesce and yields an error
75
+ * @returns The instantiated model on success
76
+ */
77
+ declare function hydrateModel(ast: CloesceAst, constructorRegistry: ModelConstructorRegistry, d1: D1Database, model: Model, id: string, dataSource: string | null): Promise<Either<HttpResult, object>>;
78
+ /**
79
+ * Calls a method on a model given a list of parameters.
80
+ * @returns 500 on an uncaught client error, 200 with a result body on success
81
+ */
82
+ declare function methodDispatch(instance: InstantiatedUserDefinedModel, instanceRegistry: InstanceRegistry, envMeta: MetaWranglerEnv, method: ModelMethod, params: Record<string, unknown>): Promise<HttpResult<unknown>>;
83
+ /**
84
+ * Actual implementation of sql to model mapping.
85
+ *
86
+ * TODO: If we don't want to write this in every language, would it be possible to create a
87
+ * single WASM binary for this method?
88
+ *
89
+ * @throws generic errors if the metadata is missing some value
90
+ */
91
+ declare function _modelsFromSql(modelName: string, ast: CloesceAst, constructorRegistry: ModelConstructorRegistry, records: Record<string, any>[], includeTree: Record<string, UserDefinedModel> | null): InstantiatedUserDefinedModel[];
92
+ interface MatchedRoute {
93
+ model: Model;
94
+ method: ModelMethod;
95
+ id: string | null;
96
+ }
97
+ /**
98
+ * Each individual state of the `cloesce` function for testing purposes.
99
+ */
100
+ export declare const _cloesceInternal: {
101
+ matchRoute: typeof matchRoute;
102
+ validateRequest: typeof validateRequest;
103
+ hydrateModel: typeof hydrateModel;
104
+ methodDispatch: typeof methodDispatch;
105
+ _modelsFromSql: typeof _modelsFromSql;
106
+ };
107
+ export {};
108
+ //# sourceMappingURL=cloesce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloesce.d.ts","sourceRoot":"","sources":["../src/cloesce.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,453 @@
1
+ import { left, right, isNullableType, getNavigationPropertyCidlType, } from "./common.js";
2
+ /**
3
+ * Singleton instances of the MetaCidl and Constructor Registry.
4
+ * These values are guaranteed to never change throughout a workers lifetime.
5
+ */
6
+ class MetaContainer {
7
+ ast;
8
+ constructorRegistry;
9
+ static instance;
10
+ constructor(ast, constructorRegistry) {
11
+ this.ast = ast;
12
+ this.constructorRegistry = constructorRegistry;
13
+ }
14
+ static init(ast, constructorRegistry) {
15
+ if (!this.instance) {
16
+ this.instance = new MetaContainer(ast, constructorRegistry);
17
+ }
18
+ }
19
+ static get() {
20
+ return this.instance;
21
+ }
22
+ }
23
+ /**
24
+ * Creates model instances given a properly formatted SQL record
25
+ * (either a foreign-key-less model or derived from a Cloesce generated view)
26
+ * @param ctor The type of the model
27
+ * @param records SQL records
28
+ * @param includeTree The include tree to use when parsing the records
29
+ * @returns
30
+ */
31
+ export function modelsFromSql(ctor, records, includeTree) {
32
+ const { ast, constructorRegistry } = MetaContainer.get();
33
+ return _modelsFromSql(ctor.name, ast, constructorRegistry, records, includeTree);
34
+ }
35
+ /**
36
+ * Cloesce entry point. Given a request, undergoes routing, validating,
37
+ * hydrating, and method dispatch.
38
+ * @param ast The CIDL AST
39
+ * @param constructorRegistry A mapping of user defined class names to their respective constructor
40
+ * @param instanceRegistry A mapping of a dependency class name to its instantiated object.
41
+ * @param request An incoming request to the workers server
42
+ * @param api_route The url's path to the api, e.g. api/v1/fooapi/
43
+ * @param envMeta Meta information on the wrangler env and D1 databases
44
+ * @returns A Response with an `HttpResult` JSON body.
45
+ */
46
+ export async function cloesce(request, ast, constructorRegistry, instanceRegistry, envMeta, api_route) {
47
+ MetaContainer.init(ast, constructorRegistry);
48
+ const d1 = instanceRegistry.get(envMeta.envName)[envMeta.dbName];
49
+ // Match the route to a model method
50
+ const route = matchRoute(request, ast, api_route);
51
+ if (!route.ok) {
52
+ return toResponse(route.value);
53
+ }
54
+ const { method, model, id } = route.value;
55
+ // Validate request body to the model method
56
+ const isValidRequest = await validateRequest(request, ast, model, method, id);
57
+ if (!isValidRequest.ok) {
58
+ return toResponse(isValidRequest.value);
59
+ }
60
+ const [requestParamMap, dataSource] = isValidRequest.value;
61
+ // Instantatiate the model
62
+ let instance;
63
+ if (method.is_static) {
64
+ instance = constructorRegistry[model.name];
65
+ }
66
+ else {
67
+ const successfulModel = await hydrateModel(ast, constructorRegistry, d1, model, id, dataSource);
68
+ if (!successfulModel.ok) {
69
+ return toResponse(successfulModel.value);
70
+ }
71
+ instance = successfulModel.value;
72
+ }
73
+ // Dispatch a method on the model and return the result
74
+ return toResponse(await methodDispatch(instance, instanceRegistry, envMeta, method, requestParamMap));
75
+ }
76
+ /**
77
+ * Matches a request to a method on a model.
78
+ * @param api_route The route from the domain to the actual API, ie https://foo.com/route/to/api => route/to/api/
79
+ * @returns 404 or a `MatchedRoute`
80
+ */
81
+ function matchRoute(request, ast, api_route) {
82
+ const url = new URL(request.url);
83
+ const notFound = (e) => left(errorState(404, `Path not found: ${e} ${url.pathname}`));
84
+ const routeParts = url.pathname
85
+ .slice(api_route.length)
86
+ .split("/")
87
+ .filter(Boolean);
88
+ if (routeParts.length < 2) {
89
+ return notFound("Expected /model/method or /model/:id/method");
90
+ }
91
+ // Attempt to extract from routeParts
92
+ const modelName = routeParts[0];
93
+ const methodName = routeParts[routeParts.length - 1];
94
+ const id = routeParts.length === 3 ? routeParts[1] : null;
95
+ const model = ast.models[modelName];
96
+ if (!model) {
97
+ return notFound(`Unknown model ${modelName}`);
98
+ }
99
+ const method = model.methods[methodName];
100
+ if (!method) {
101
+ return notFound(`Unknown method ${modelName}.${methodName}`);
102
+ }
103
+ if (request.method !== method.http_verb) {
104
+ return notFound("Unmatched HTTP method");
105
+ }
106
+ return right({
107
+ model,
108
+ method,
109
+ id,
110
+ });
111
+ }
112
+ /**
113
+ * Validates the request's body/search params against a ModelMethod
114
+ * @returns 400 or a `RequestParamMap` consisting of each parameters name mapped to its value, and
115
+ * a data source
116
+ */
117
+ async function validateRequest(request, ast, model, method, id) {
118
+ // Error state: any missing parameter, body, or malformed input will exit with 400.
119
+ const invalidRequest = (e) => left(errorState(400, `Invalid Request Body: ${e}`));
120
+ if (!method.is_static && id == null) {
121
+ return invalidRequest("Id's are required for instantiated methods.");
122
+ }
123
+ // Filter out any injected parameters that will not be passed
124
+ // by the query.
125
+ const requiredParams = method.parameters.filter((p) => !(typeof p.cidl_type === "object" &&
126
+ p.cidl_type !== null &&
127
+ "Inject" in p.cidl_type));
128
+ // Extract data source
129
+ const url = new URL(request.url);
130
+ let dataSource = url.searchParams.get("dataSource");
131
+ // Extract url or body parameters
132
+ let requestBodyMap;
133
+ if (method.http_verb === "GET") {
134
+ requestBodyMap = Object.fromEntries(url.searchParams.entries());
135
+ }
136
+ else {
137
+ try {
138
+ requestBodyMap = await request.json();
139
+ }
140
+ catch {
141
+ return invalidRequest("Could not retrieve JSON body.");
142
+ }
143
+ }
144
+ // Validate data source if exists
145
+ if (dataSource && !(dataSource in model.data_sources)) {
146
+ return invalidRequest(`Unknown data source ${dataSource}`);
147
+ }
148
+ // Ensure all required params exist
149
+ if (!requiredParams.every((p) => p.name in requestBodyMap)) {
150
+ return invalidRequest(`Missing parameters.`);
151
+ }
152
+ // Validate all parameters type
153
+ for (const p of requiredParams) {
154
+ const value = requestBodyMap[p.name];
155
+ if (!validateCidlType(ast, value, p.cidl_type)) {
156
+ return invalidRequest("Invalid parameters.");
157
+ }
158
+ }
159
+ return right([requestBodyMap, dataSource]);
160
+ }
161
+ /**
162
+ * Queries D1 for a particular model's ID, then transforms the SQL column output into
163
+ * an instance of a model using the provided include tree and metadata as a guide.
164
+ * @returns 404 if no record was found for the provided ID
165
+ * @returns 500 if the D1 database is not synced with Cloesce and yields an error
166
+ * @returns The instantiated model on success
167
+ */
168
+ async function hydrateModel(ast, constructorRegistry, d1, model, id, dataSource) {
169
+ // Error state: If the D1 database has been tweaked outside of Cloesce
170
+ // resulting in a malformed query, exit with a 500.
171
+ const malformedQuery = (e) => left(errorState(500, `Error in hydration query, is the database out of sync with the backend?: ${e instanceof Error ? e.message : String(e)}`));
172
+ // Error state: If no record is found for the id, return a 404
173
+ const missingRecord = left(errorState(404, "Record not found"));
174
+ const pk = model.primary_key.name;
175
+ const query = dataSource !== null
176
+ ? `SELECT * FROM "${model.name}.${dataSource}" WHERE "${model.name}.${pk}" = ?`
177
+ : `SELECT * FROM "${model.name}" WHERE "${pk}" = ?`;
178
+ // Query DB
179
+ let records;
180
+ try {
181
+ records = await d1.prepare(query).bind(id).run();
182
+ if (!records) {
183
+ return missingRecord;
184
+ }
185
+ if (records.error) {
186
+ return malformedQuery(records.error);
187
+ }
188
+ }
189
+ catch (e) {
190
+ return malformedQuery(e);
191
+ }
192
+ // Get include tree
193
+ const includeTree = dataSource !== null ? model.data_sources[dataSource].tree : {};
194
+ // Hydrate
195
+ const models = _modelsFromSql(model.name, ast, constructorRegistry, records.results, includeTree);
196
+ console.log(JSON.stringify(models));
197
+ return right(models[0]);
198
+ }
199
+ /**
200
+ * Calls a method on a model given a list of parameters.
201
+ * @returns 500 on an uncaught client error, 200 with a result body on success
202
+ */
203
+ async function methodDispatch(instance, instanceRegistry, envMeta, method, params) {
204
+ // Error state: Client code ran into an uncaught exception.
205
+ const uncaughtException = (e) => errorState(500, `Uncaught exception in method dispatch: ${e instanceof Error ? e.message : String(e)}`);
206
+ // For now, the only injected dependency is the wrangler env,
207
+ // so we will assume that is what this is
208
+ const paramArray = method.parameters.map((p) => params[p.name] == undefined
209
+ ? instanceRegistry.get(envMeta.envName)
210
+ : params[p.name]);
211
+ // Ensure the result is always some HttpResult
212
+ const resultWrapper = (res) => {
213
+ const rt = method.return_type;
214
+ if (rt === null) {
215
+ return { ok: true, status: 200 };
216
+ }
217
+ if (typeof rt === "object" && rt !== null && "HttpResult" in rt) {
218
+ return res;
219
+ }
220
+ return { ok: true, status: 200, data: res };
221
+ };
222
+ try {
223
+ return resultWrapper(await instance[method.name](...paramArray));
224
+ }
225
+ catch (e) {
226
+ return uncaughtException(e);
227
+ }
228
+ }
229
+ function validateCidlType(ast, value, cidlType) {
230
+ if (value === undefined)
231
+ return false;
232
+ // TODO: consequences of null checking like this? 'null' is passed in
233
+ // as a string for GET requests...
234
+ const nullable = isNullableType(cidlType);
235
+ if (value == null || value === "null")
236
+ return nullable;
237
+ if (nullable) {
238
+ cidlType = cidlType.Nullable; // Unwrap the nullable type
239
+ }
240
+ // Handle primitive string types with switch
241
+ if (typeof cidlType === "string") {
242
+ switch (cidlType) {
243
+ case "Integer":
244
+ return Number.isInteger(Number(value));
245
+ case "Real":
246
+ return !Number.isNaN(Number(value));
247
+ case "Text":
248
+ return typeof value === "string";
249
+ case "Blob":
250
+ return value instanceof Blob || value instanceof ArrayBuffer;
251
+ default:
252
+ return false;
253
+ }
254
+ }
255
+ // Handle Models
256
+ if ("Object" in cidlType && ast.models[cidlType.Object]) {
257
+ const model = ast.models[cidlType.Object];
258
+ if (!model || typeof value !== "object")
259
+ return false;
260
+ const valueObj = value;
261
+ // Validate attributes
262
+ if (!model.attributes.every((attr) => validateCidlType(ast, valueObj[attr.value.name], attr.value.cidl_type))) {
263
+ return false;
264
+ }
265
+ // Validate navigation properties
266
+ return model.navigation_properties.every((nav) => {
267
+ const navValue = valueObj[nav.var_name];
268
+ return (navValue == null ||
269
+ validateCidlType(ast, navValue, getNavigationPropertyCidlType(nav)));
270
+ });
271
+ }
272
+ // Handle Plain Old Objects
273
+ if ("Object" in cidlType && ast.poos[cidlType.Object]) {
274
+ const poo = ast.poos[cidlType.Object];
275
+ if (!poo || typeof value !== "object")
276
+ return false;
277
+ const valueObj = value;
278
+ // Validate attributes
279
+ if (!poo.attributes.every((attr) => validateCidlType(ast, valueObj[attr.name], attr.cidl_type))) {
280
+ return false;
281
+ }
282
+ }
283
+ if ("Array" in cidlType) {
284
+ const arr = cidlType.Array;
285
+ return (Array.isArray(value) && value.every((v) => validateCidlType(ast, v, arr)));
286
+ }
287
+ if ("HttpResult" in cidlType) {
288
+ if (value === null)
289
+ return cidlType.HttpResult === null;
290
+ if (cidlType.HttpResult === null)
291
+ return false;
292
+ return validateCidlType(ast, value, cidlType.HttpResult);
293
+ }
294
+ return false;
295
+ }
296
+ /**
297
+ * Actual implementation of sql to model mapping.
298
+ *
299
+ * TODO: If we don't want to write this in every language, would it be possible to create a
300
+ * single WASM binary for this method?
301
+ *
302
+ * @throws generic errors if the metadata is missing some value
303
+ */
304
+ // Main function that creates instances from SQL records
305
+ function _modelsFromSql(modelName, ast, constructorRegistry, records, includeTree) {
306
+ const model = ast.models[modelName];
307
+ if (!model)
308
+ return [];
309
+ const Constructor = constructorRegistry[modelName];
310
+ if (!Constructor)
311
+ return [];
312
+ const pkName = model.primary_key.name;
313
+ const resultMap = new Map();
314
+ for (const record of records) {
315
+ const pkValue = record[`${modelName}.${pkName}`] ?? record[pkName];
316
+ if (pkValue == null)
317
+ continue;
318
+ let instance = resultMap.get(pkValue);
319
+ if (!instance) {
320
+ instance = new Constructor();
321
+ instance[pkName] = pkValue;
322
+ // Set scalar attributes
323
+ for (const attr of model.attributes) {
324
+ const attrName = attr.value.name;
325
+ const prefixedKey = `${modelName}.${attrName}`;
326
+ const nonPrefixedKey = attrName;
327
+ if (prefixedKey in record) {
328
+ instance[attrName] = record[prefixedKey];
329
+ }
330
+ else if (nonPrefixedKey in record) {
331
+ instance[attrName] = record[nonPrefixedKey];
332
+ }
333
+ }
334
+ // Initialize ALL navigation properties at root level
335
+ // If not in include tree, initialize OneToMany and ManyToMany as empty arrays
336
+ for (const navProp of model.navigation_properties) {
337
+ if ("OneToMany" in navProp.kind || "ManyToMany" in navProp.kind) {
338
+ // Always initialize OneToMany and ManyToMany as empty arrays
339
+ instance[navProp.var_name] = [];
340
+ }
341
+ // OneToOne properties left as undefined unless populated
342
+ }
343
+ resultMap.set(pkValue, instance);
344
+ }
345
+ // Process navigation properties that are in the include tree
346
+ if (includeTree) {
347
+ processNavigationProperties(instance, model, modelName, includeTree, record, ast, constructorRegistry);
348
+ }
349
+ }
350
+ return Array.from(resultMap.values());
351
+ }
352
+ function processNavigationProperties(instance, model, prefix, includeTree, record, ast, constructorRegistry) {
353
+ for (const navProp of model.navigation_properties) {
354
+ if (!(navProp.var_name in includeTree)) {
355
+ continue;
356
+ }
357
+ const nestedModel = ast.models[navProp.model_name];
358
+ if (!nestedModel) {
359
+ continue;
360
+ }
361
+ // Extract nested model's primary key - check both prefixed and non-prefixed
362
+ const nestedPkName = nestedModel.primary_key.name;
363
+ const prefixedNestedPkKey = `${prefix}.${navProp.var_name}.${nestedPkName}`;
364
+ const nonPrefixedNestedPkKey = `${navProp.var_name}.${nestedPkName}`;
365
+ const nestedPkValue = record[prefixedNestedPkKey] ?? record[nonPrefixedNestedPkKey];
366
+ if (nestedPkValue == null) {
367
+ continue; // No nested object in this row
368
+ }
369
+ // Determine if this is OneToMany/ManyToMany or OneToOne
370
+ const isOneToMany = "OneToMany" in navProp.kind || "ManyToMany" in navProp.kind;
371
+ // Check if we already added this nested object (for OneToMany)
372
+ if (isOneToMany) {
373
+ const navArray = instance[navProp.var_name];
374
+ const alreadyExists = navArray.some((item) => item[nestedPkName] === nestedPkValue);
375
+ if (alreadyExists) {
376
+ continue;
377
+ }
378
+ }
379
+ else {
380
+ // For OneToOne, check if already set
381
+ if (instance[navProp.var_name] != null) {
382
+ continue;
383
+ }
384
+ }
385
+ const NestedConstructor = constructorRegistry[navProp.model_name];
386
+ if (!NestedConstructor) {
387
+ continue;
388
+ }
389
+ const nestedInstance = new NestedConstructor();
390
+ nestedInstance[nestedPkName] = nestedPkValue;
391
+ // Assign nested scalar attributes - check both prefixed and non-prefixed
392
+ for (const nestedAttr of nestedModel.attributes) {
393
+ const nestedAttrName = nestedAttr.value.name;
394
+ const prefixedKey = `${prefix}.${navProp.var_name}.${nestedAttrName}`;
395
+ const nonPrefixedKey = `${navProp.var_name}.${nestedAttrName}`;
396
+ // Check prefixed key first, then non-prefixed
397
+ if (prefixedKey in record) {
398
+ nestedInstance[nestedAttrName] = record[prefixedKey];
399
+ }
400
+ else if (nonPrefixedKey in record) {
401
+ nestedInstance[nestedAttrName] = record[nonPrefixedKey];
402
+ }
403
+ }
404
+ // Initialize ALL navigation properties on the nested instance
405
+ // If not in include tree, initialize OneToMany and ManyToMany as empty arrays
406
+ const nestedIncludeTree = includeTree[navProp.var_name];
407
+ for (const nestedNavProp of nestedModel.navigation_properties) {
408
+ const isInIncludeTree = nestedIncludeTree &&
409
+ typeof nestedIncludeTree === "object" &&
410
+ nestedNavProp.var_name in nestedIncludeTree;
411
+ if ("OneToMany" in nestedNavProp.kind ||
412
+ "ManyToMany" in nestedNavProp.kind) {
413
+ // Always initialize OneToMany and ManyToMany as arrays (empty if not in include tree)
414
+ nestedInstance[nestedNavProp.var_name] = [];
415
+ }
416
+ else if (!isInIncludeTree) {
417
+ // OneToOne not in include tree - leave as undefined or null
418
+ // Will be set during recursive processing if in include tree
419
+ }
420
+ }
421
+ // Recursively process nested navigation properties that are in the include tree
422
+ if (nestedIncludeTree && typeof nestedIncludeTree === "object") {
423
+ processNavigationProperties(nestedInstance, nestedModel, `${prefix}.${navProp.var_name}`, nestedIncludeTree, record, ast, constructorRegistry);
424
+ }
425
+ // Assign the nested instance based on relationship type
426
+ if (isOneToMany) {
427
+ instance[navProp.var_name].push(nestedInstance);
428
+ }
429
+ else {
430
+ // OneToOne - assign directly
431
+ instance[navProp.var_name] = nestedInstance;
432
+ }
433
+ }
434
+ }
435
+ function errorState(status, message) {
436
+ return { ok: false, status, message };
437
+ }
438
+ function toResponse(r) {
439
+ return new Response(JSON.stringify(r), {
440
+ status: r.status,
441
+ headers: { "Content-Type": "application/json" },
442
+ });
443
+ }
444
+ /**
445
+ * Each individual state of the `cloesce` function for testing purposes.
446
+ */
447
+ export const _cloesceInternal = {
448
+ matchRoute,
449
+ validateRequest,
450
+ hydrateModel,
451
+ methodDispatch,
452
+ _modelsFromSql,
453
+ };
@@ -0,0 +1,96 @@
1
+ export type Either<L, R> = {
2
+ ok: false;
3
+ value: L;
4
+ } | {
5
+ ok: true;
6
+ value: R;
7
+ };
8
+ export declare function left<L>(value: L): Either<L, never>;
9
+ export declare function right<R>(value: R): Either<never, R>;
10
+ export type HttpResult<T = unknown> = {
11
+ ok: boolean;
12
+ status: number;
13
+ data?: T;
14
+ message?: string;
15
+ };
16
+ export type CidlType = "Void" | "Integer" | "Real" | "Text" | "Blob" | {
17
+ Inject: string;
18
+ } | {
19
+ Model: string;
20
+ } | {
21
+ Nullable: CidlType;
22
+ } | {
23
+ Array: CidlType;
24
+ } | {
25
+ HttpResult: CidlType;
26
+ };
27
+ export declare function isNullableType(ty: CidlType): boolean;
28
+ export declare enum HttpVerb {
29
+ GET = "GET",
30
+ POST = "POST",
31
+ PUT = "PUT",
32
+ PATCH = "PATCH",
33
+ DELETE = "DELETE"
34
+ }
35
+ export interface NamedTypedValue {
36
+ name: string;
37
+ cidl_type: CidlType;
38
+ }
39
+ export interface ModelAttribute {
40
+ value: NamedTypedValue;
41
+ foreign_key_reference: string | null;
42
+ }
43
+ export interface ModelMethod {
44
+ name: string;
45
+ is_static: boolean;
46
+ http_verb: HttpVerb;
47
+ return_type: CidlType | null;
48
+ parameters: NamedTypedValue[];
49
+ }
50
+ export type NavigationPropertyKind = {
51
+ OneToOne: {
52
+ reference: string;
53
+ };
54
+ } | {
55
+ OneToMany: {
56
+ reference: string;
57
+ };
58
+ } | {
59
+ ManyToMany: {
60
+ unique_id: string;
61
+ };
62
+ };
63
+ export interface NavigationProperty {
64
+ var_name: string;
65
+ model_name: string;
66
+ kind: NavigationPropertyKind;
67
+ }
68
+ export declare function getNavigationPropertyCidlType(nav: NavigationProperty): CidlType;
69
+ export interface Model {
70
+ name: string;
71
+ primary_key: NamedTypedValue;
72
+ attributes: ModelAttribute[];
73
+ navigation_properties: NavigationProperty[];
74
+ methods: Record<string, ModelMethod>;
75
+ data_sources: Record<string, DataSource>;
76
+ source_path: string;
77
+ }
78
+ export interface CidlIncludeTree {
79
+ [key: string]: CidlIncludeTree;
80
+ }
81
+ export interface DataSource {
82
+ name: string;
83
+ tree: CidlIncludeTree;
84
+ }
85
+ export interface WranglerEnv {
86
+ name: string;
87
+ source_path: string;
88
+ }
89
+ export interface CloesceAst {
90
+ version: string;
91
+ project_name: string;
92
+ language: "TypeScript";
93
+ wrangler_env: WranglerEnv;
94
+ models: Record<string, Model>;
95
+ }
96
+ //# sourceMappingURL=common.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../src/common.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,MAAM,CAAC,CAAC,EAAE,CAAC,IAAI;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,CAAC;AAC5E,wBAAgB,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,CAElD;AACD,wBAAgB,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAEnD;AAED,MAAM,MAAM,UAAU,CAAC,CAAC,GAAG,OAAO,IAAI;IACpC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,CAAC,CAAC;IACT,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,QAAQ,GAChB,MAAM,GACN,SAAS,GACT,MAAM,GACN,MAAM,GACN,MAAM,GACN;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GACjB;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,GACtB;IAAE,KAAK,EAAE,QAAQ,CAAA;CAAE,GACnB;IAAE,UAAU,EAAE,QAAQ,CAAA;CAAE,CAAC;AAE7B,wBAAgB,cAAc,CAAC,EAAE,EAAE,QAAQ,GAAG,OAAO,CAEpD;AAED,oBAAY,QAAQ;IAClB,GAAG,QAAQ;IACX,IAAI,SAAS;IACb,GAAG,QAAQ;IACX,KAAK,UAAU;IACf,MAAM,WAAW;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,QAAQ,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,eAAe,CAAC;IACvB,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,QAAQ,CAAC;IACpB,WAAW,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC7B,UAAU,EAAE,eAAe,EAAE,CAAC;CAC/B;AAED,MAAM,MAAM,sBAAsB,GAC9B;IAAE,QAAQ,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GACnC;IAAE,SAAS,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GACpC;IAAE,UAAU,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAE1C,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,sBAAsB,CAAC;CAC9B;AAED,wBAAgB,6BAA6B,CAC3C,GAAG,EAAE,kBAAkB,GACtB,QAAQ,CAIV;AAED,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,eAAe,CAAC;IAC7B,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,qBAAqB,EAAE,kBAAkB,EAAE,CAAC;IAC5C,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACrC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACzC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAAC;CAChC;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,eAAe,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,YAAY,CAAC;IACvB,YAAY,EAAE,WAAW,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;CAC/B"}