@stonyx/orm 0.2.5-alpha.0 → 0.3.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.
Files changed (166) hide show
  1. package/README.md +482 -15
  2. package/config/environment.js +63 -6
  3. package/dist/aggregates.d.ts +21 -0
  4. package/dist/aggregates.js +93 -0
  5. package/dist/attr.d.ts +2 -0
  6. package/dist/attr.js +22 -0
  7. package/dist/belongs-to.d.ts +11 -0
  8. package/dist/belongs-to.js +59 -0
  9. package/dist/cli.d.ts +22 -0
  10. package/dist/cli.js +148 -0
  11. package/dist/commands.d.ts +7 -0
  12. package/dist/commands.js +146 -0
  13. package/dist/db.d.ts +21 -0
  14. package/dist/db.js +180 -0
  15. package/dist/exports/db.d.ts +7 -0
  16. package/{src → dist}/exports/db.js +2 -4
  17. package/dist/has-many.d.ts +11 -0
  18. package/dist/has-many.js +58 -0
  19. package/dist/hooks.d.ts +75 -0
  20. package/dist/hooks.js +110 -0
  21. package/dist/index.d.ts +14 -0
  22. package/dist/index.js +34 -0
  23. package/dist/main.d.ts +46 -0
  24. package/dist/main.js +181 -0
  25. package/dist/manage-record.d.ts +13 -0
  26. package/dist/manage-record.js +123 -0
  27. package/dist/meta-request.d.ts +6 -0
  28. package/dist/meta-request.js +52 -0
  29. package/dist/migrate.d.ts +2 -0
  30. package/dist/migrate.js +57 -0
  31. package/dist/model-property.d.ts +9 -0
  32. package/dist/model-property.js +29 -0
  33. package/dist/model.d.ts +15 -0
  34. package/dist/model.js +18 -0
  35. package/dist/mysql/connection.d.ts +14 -0
  36. package/dist/mysql/connection.js +24 -0
  37. package/dist/mysql/migration-generator.d.ts +45 -0
  38. package/dist/mysql/migration-generator.js +254 -0
  39. package/dist/mysql/migration-runner.d.ts +12 -0
  40. package/dist/mysql/migration-runner.js +88 -0
  41. package/dist/mysql/mysql-db.d.ts +100 -0
  42. package/dist/mysql/mysql-db.js +425 -0
  43. package/dist/mysql/query-builder.d.ts +10 -0
  44. package/dist/mysql/query-builder.js +44 -0
  45. package/dist/mysql/schema-introspector.d.ts +19 -0
  46. package/dist/mysql/schema-introspector.js +257 -0
  47. package/dist/mysql/type-map.d.ts +21 -0
  48. package/dist/mysql/type-map.js +36 -0
  49. package/dist/orm-request.d.ts +38 -0
  50. package/dist/orm-request.js +475 -0
  51. package/dist/plural-registry.d.ts +4 -0
  52. package/dist/plural-registry.js +9 -0
  53. package/dist/postgres/connection.d.ts +15 -0
  54. package/dist/postgres/connection.js +32 -0
  55. package/dist/postgres/migration-generator.d.ts +45 -0
  56. package/dist/postgres/migration-generator.js +280 -0
  57. package/dist/postgres/migration-runner.d.ts +10 -0
  58. package/dist/postgres/migration-runner.js +87 -0
  59. package/dist/postgres/postgres-db.d.ts +119 -0
  60. package/dist/postgres/postgres-db.js +477 -0
  61. package/dist/postgres/query-builder.d.ts +27 -0
  62. package/dist/postgres/query-builder.js +98 -0
  63. package/dist/postgres/schema-introspector.d.ts +29 -0
  64. package/dist/postgres/schema-introspector.js +296 -0
  65. package/dist/postgres/type-map.d.ts +23 -0
  66. package/dist/postgres/type-map.js +56 -0
  67. package/dist/record.d.ts +75 -0
  68. package/dist/record.js +129 -0
  69. package/dist/relationships.d.ts +10 -0
  70. package/dist/relationships.js +41 -0
  71. package/dist/schema-helpers.d.ts +20 -0
  72. package/dist/schema-helpers.js +48 -0
  73. package/dist/serializer.d.ts +17 -0
  74. package/dist/serializer.js +136 -0
  75. package/dist/setup-rest-server.d.ts +1 -0
  76. package/dist/setup-rest-server.js +52 -0
  77. package/dist/standalone-db.d.ts +58 -0
  78. package/dist/standalone-db.js +142 -0
  79. package/dist/store.d.ts +62 -0
  80. package/dist/store.js +286 -0
  81. package/dist/timescale/query-builder.d.ts +43 -0
  82. package/dist/timescale/query-builder.js +115 -0
  83. package/dist/timescale/timescale-db.d.ts +45 -0
  84. package/dist/timescale/timescale-db.js +84 -0
  85. package/dist/transforms.d.ts +2 -0
  86. package/dist/transforms.js +17 -0
  87. package/dist/types/orm-types.d.ts +153 -0
  88. package/dist/types/orm-types.js +1 -0
  89. package/dist/utils.d.ts +7 -0
  90. package/dist/utils.js +17 -0
  91. package/dist/view-resolver.d.ts +8 -0
  92. package/dist/view-resolver.js +171 -0
  93. package/dist/view.d.ts +11 -0
  94. package/dist/view.js +18 -0
  95. package/package.json +64 -11
  96. package/src/aggregates.ts +109 -0
  97. package/src/{attr.js → attr.ts} +2 -2
  98. package/src/belongs-to.ts +90 -0
  99. package/src/cli.ts +183 -0
  100. package/src/commands.ts +179 -0
  101. package/src/db.ts +232 -0
  102. package/src/exports/db.ts +7 -0
  103. package/src/has-many.ts +92 -0
  104. package/src/hooks.ts +151 -0
  105. package/src/{index.js → index.ts} +12 -2
  106. package/src/main.ts +229 -0
  107. package/src/manage-record.ts +161 -0
  108. package/src/{meta-request.js → meta-request.ts} +17 -14
  109. package/src/migrate.ts +72 -0
  110. package/src/model-property.ts +35 -0
  111. package/src/model.ts +21 -0
  112. package/src/mysql/connection.ts +43 -0
  113. package/src/mysql/migration-generator.ts +337 -0
  114. package/src/mysql/migration-runner.ts +121 -0
  115. package/src/mysql/mysql-db.ts +543 -0
  116. package/src/mysql/query-builder.ts +69 -0
  117. package/src/mysql/schema-introspector.ts +310 -0
  118. package/src/mysql/type-map.ts +42 -0
  119. package/src/orm-request.ts +582 -0
  120. package/src/plural-registry.ts +12 -0
  121. package/src/postgres/connection.ts +48 -0
  122. package/src/postgres/migration-generator.ts +370 -0
  123. package/src/postgres/migration-runner.ts +115 -0
  124. package/src/postgres/postgres-db.ts +616 -0
  125. package/src/postgres/query-builder.ts +148 -0
  126. package/src/postgres/schema-introspector.ts +360 -0
  127. package/src/postgres/type-map.ts +61 -0
  128. package/src/record.ts +186 -0
  129. package/src/relationships.ts +54 -0
  130. package/src/schema-helpers.ts +59 -0
  131. package/src/serializer.ts +161 -0
  132. package/src/setup-rest-server.ts +62 -0
  133. package/src/standalone-db.ts +185 -0
  134. package/src/store.ts +373 -0
  135. package/src/timescale/query-builder.ts +174 -0
  136. package/src/timescale/timescale-db.ts +119 -0
  137. package/src/transforms.ts +20 -0
  138. package/src/types/mysql2.d.ts +49 -0
  139. package/src/types/orm-types.ts +158 -0
  140. package/src/types/pg.d.ts +32 -0
  141. package/src/types/stonyx-cron.d.ts +5 -0
  142. package/src/types/stonyx-events.d.ts +4 -0
  143. package/src/types/stonyx-rest-server.d.ts +16 -0
  144. package/src/types/stonyx-utils.d.ts +33 -0
  145. package/src/types/stonyx.d.ts +21 -0
  146. package/src/utils.ts +22 -0
  147. package/src/view-resolver.ts +211 -0
  148. package/src/view.ts +22 -0
  149. package/.claude/project-structure.md +0 -578
  150. package/.github/workflows/ci.yml +0 -36
  151. package/.github/workflows/publish.yml +0 -143
  152. package/src/belongs-to.js +0 -63
  153. package/src/db.js +0 -80
  154. package/src/has-many.js +0 -61
  155. package/src/main.js +0 -119
  156. package/src/manage-record.js +0 -103
  157. package/src/model-property.js +0 -29
  158. package/src/model.js +0 -9
  159. package/src/orm-request.js +0 -249
  160. package/src/record.js +0 -100
  161. package/src/relationships.js +0 -43
  162. package/src/serializer.js +0 -138
  163. package/src/setup-rest-server.js +0 -57
  164. package/src/store.js +0 -211
  165. package/src/transforms.js +0 -20
  166. package/stonyx-bootstrap.cjs +0 -30
@@ -0,0 +1,475 @@
1
+ import { Request } from '@stonyx/rest-server';
2
+ import Orm, { store, createRecord, updateRecord } from '@stonyx/orm';
3
+ import { camelCaseToKebabCase } from '@stonyx/utils/string';
4
+ import { getPluralName } from './plural-registry.js';
5
+ import { getBeforeHooks, getAfterHooks } from './hooks.js';
6
+ import config from 'stonyx/config';
7
+ import { isOrmRecord } from './utils.js';
8
+ const methodAccessMap = {
9
+ GET: 'read',
10
+ POST: 'create',
11
+ DELETE: 'delete',
12
+ PATCH: 'update',
13
+ };
14
+ const WRITE_OPERATIONS = new Set(['create', 'update', 'delete']);
15
+ // Helper to detect relationship type from function
16
+ function getRelationshipInfo(property) {
17
+ if (typeof property !== 'function')
18
+ return null;
19
+ const relType = property.__relationshipType;
20
+ if (relType === 'belongsTo') {
21
+ return { type: 'belongsTo', isArray: false };
22
+ }
23
+ if (relType === 'hasMany') {
24
+ return { type: 'hasMany', isArray: true };
25
+ }
26
+ return null;
27
+ }
28
+ // Helper to introspect model relationships
29
+ function getModelRelationships(modelName) {
30
+ const { modelClass } = Orm.instance.getRecordClasses(modelName);
31
+ if (!modelClass)
32
+ return {};
33
+ const model = new modelClass(modelName);
34
+ const relationships = {};
35
+ for (const [key, property] of Object.entries(model)) {
36
+ if (key.startsWith('__'))
37
+ continue;
38
+ const info = getRelationshipInfo(property);
39
+ if (info) {
40
+ relationships[key] = info;
41
+ }
42
+ }
43
+ return relationships;
44
+ }
45
+ // Helper to build base URL from request
46
+ function getBaseUrl(request) {
47
+ const protocol = request.protocol || 'http';
48
+ const host = request.get('host');
49
+ return `${protocol}://${host}`;
50
+ }
51
+ function getId(params) {
52
+ const id = params.id;
53
+ if (!id)
54
+ return '';
55
+ if (isNaN(id))
56
+ return id;
57
+ return parseInt(id);
58
+ }
59
+ function buildResponse(data, includeParam, recordOrRecords, options = {}) {
60
+ const { links, baseUrl } = options;
61
+ const response = { data };
62
+ // Add top-level links
63
+ if (links) {
64
+ response.links = links;
65
+ }
66
+ if (!includeParam)
67
+ return response;
68
+ const includes = parseInclude(includeParam);
69
+ if (includes.length === 0)
70
+ return response;
71
+ const includedRecords = collectIncludedRecords(recordOrRecords, includes);
72
+ if (includedRecords.length > 0) {
73
+ response.included = includedRecords.map(record => record.toJSON?.({ baseUrl }));
74
+ }
75
+ return response;
76
+ }
77
+ /**
78
+ * Recursively traverse an include path and collect related records
79
+ */
80
+ function traverseIncludePath(currentRecords, includePath, depth, seen, included) {
81
+ if (depth >= includePath.length)
82
+ return; // Reached end of path
83
+ const relationshipName = includePath[depth];
84
+ const nextRecords = [];
85
+ for (const record of currentRecords) {
86
+ if (!record.__relationships)
87
+ continue;
88
+ if (!(relationshipName in record.__relationships))
89
+ continue;
90
+ const relatedRecords = record.__relationships[relationshipName];
91
+ if (!relatedRecords)
92
+ continue;
93
+ // Handle both belongsTo (single) and hasMany (array)
94
+ const recordsToProcess = Array.isArray(relatedRecords)
95
+ ? relatedRecords.filter(isOrmRecord)
96
+ : isOrmRecord(relatedRecords) ? [relatedRecords] : [];
97
+ for (const relatedRecord of recordsToProcess) {
98
+ if (!relatedRecord)
99
+ continue;
100
+ if (!relatedRecord.__model)
101
+ continue;
102
+ const type = relatedRecord.__model.__name;
103
+ const id = relatedRecord.id;
104
+ // Initialize Set for this type if needed
105
+ let seenIds = seen.get(type);
106
+ if (!seenIds) {
107
+ seenIds = new Set();
108
+ seen.set(type, seenIds);
109
+ }
110
+ // Check if we've already seen this type+id combination
111
+ if (!seenIds.has(id)) {
112
+ seenIds.add(id);
113
+ included.push(relatedRecord);
114
+ nextRecords.push(relatedRecord); // Prepare for next depth level
115
+ }
116
+ else if (depth < includePath.length - 1) {
117
+ // Even if we've seen this record, we might need it for deeper traversal
118
+ nextRecords.push(relatedRecord);
119
+ }
120
+ }
121
+ }
122
+ // If there are more segments in the path, recursively process
123
+ if (depth < includePath.length - 1 && nextRecords.length > 0) {
124
+ traverseIncludePath(nextRecords, includePath, depth + 1, seen, included);
125
+ }
126
+ }
127
+ function collectIncludedRecords(data, includes) {
128
+ if (!includes || includes.length === 0)
129
+ return [];
130
+ if (!data)
131
+ return [];
132
+ const seen = new Map(); // Map<type, Set<id>> for deduplication
133
+ const included = [];
134
+ // Normalize to array for consistent processing
135
+ const records = Array.isArray(data) ? data : [data];
136
+ // Process each include path
137
+ for (const includePath of includes) {
138
+ traverseIncludePath(records, includePath, 0, seen, included);
139
+ }
140
+ return included;
141
+ }
142
+ function parseInclude(includeParam) {
143
+ if (!includeParam || typeof includeParam !== 'string')
144
+ return [];
145
+ return includeParam
146
+ .split(',')
147
+ .map(rel => rel.trim())
148
+ .filter(rel => rel.length > 0)
149
+ .map(rel => rel.split('.')); // Parse nested paths: "owner.pets" -> ["owner", "pets"]
150
+ }
151
+ function parseFields(query) {
152
+ const fields = new Map();
153
+ if (!query)
154
+ return fields;
155
+ for (const [key, value] of Object.entries(query)) {
156
+ const match = key.match(/^fields\[(\w+)\]$/);
157
+ if (match && typeof value === 'string') {
158
+ const modelName = match[1];
159
+ const fieldNames = value.split(',').map(f => f.trim()).filter(f => f);
160
+ fields.set(modelName, new Set(fieldNames));
161
+ }
162
+ }
163
+ return fields;
164
+ }
165
+ function parseFilters(query) {
166
+ const filters = [];
167
+ if (!query)
168
+ return filters;
169
+ for (const [key, value] of Object.entries(query)) {
170
+ const match = key.match(/^filter\[(.+)\]$/);
171
+ if (match && typeof value === 'string') {
172
+ filters.push({ path: match[1].split('.'), value });
173
+ }
174
+ }
175
+ return filters;
176
+ }
177
+ function createFilterPredicate(filters) {
178
+ if (filters.length === 0)
179
+ return null;
180
+ return (record) => filters.every(({ path, value }) => {
181
+ let current = record;
182
+ for (const segment of path) {
183
+ if (current == null)
184
+ return false;
185
+ current = current[segment];
186
+ }
187
+ return String(current) === value;
188
+ });
189
+ }
190
+ export default class OrmRequest extends Request {
191
+ model;
192
+ access;
193
+ handlers;
194
+ constructor({ model, access }) {
195
+ super(...arguments);
196
+ this.model = model;
197
+ this.access = access;
198
+ const pluralizedModel = getPluralName(model);
199
+ const modelRelationships = getModelRelationships(model);
200
+ // Define raw handlers first
201
+ const getCollectionHandler = async (request, { filter: accessFilter }) => {
202
+ const allRecords = (await store.findAll(model)).filter(isOrmRecord);
203
+ const queryFilters = parseFilters(request.query);
204
+ const queryFilterPredicate = createFilterPredicate(queryFilters);
205
+ const fieldsMap = parseFields(request.query);
206
+ const modelFields = fieldsMap.get(pluralizedModel) || fieldsMap.get(model);
207
+ let recordsToReturn = allRecords;
208
+ if (accessFilter)
209
+ recordsToReturn = recordsToReturn.filter(accessFilter);
210
+ if (queryFilterPredicate)
211
+ recordsToReturn = recordsToReturn.filter(queryFilterPredicate);
212
+ const baseUrl = getBaseUrl(request);
213
+ const data = recordsToReturn.map(record => record.toJSON?.({ fields: modelFields, baseUrl }));
214
+ return buildResponse(data, request.query?.include, recordsToReturn, {
215
+ links: { self: `${baseUrl}/${pluralizedModel}` },
216
+ baseUrl
217
+ });
218
+ };
219
+ const getSingleHandler = async (request) => {
220
+ const record = await store.find(model, getId(request.params));
221
+ if (!record)
222
+ return 404;
223
+ const fieldsMap = parseFields(request.query);
224
+ const modelFields = fieldsMap.get(pluralizedModel) || fieldsMap.get(model);
225
+ const baseUrl = getBaseUrl(request);
226
+ return buildResponse(record.toJSON?.({ fields: modelFields, baseUrl }), request.query?.include, record, {
227
+ links: { self: `${baseUrl}/${pluralizedModel}/${request.params.id}` },
228
+ baseUrl
229
+ });
230
+ };
231
+ const createHandler = async ({ body, query }) => {
232
+ const { type, id, attributes, relationships: rels } = (body?.data || {});
233
+ if (!type)
234
+ return 400; // Bad request
235
+ const fieldsMap = parseFields(query);
236
+ const modelFields = fieldsMap.get(pluralizedModel) || fieldsMap.get(model);
237
+ // Check for duplicate ID
238
+ if (id !== undefined && await store.find(model, id))
239
+ return 409; // Conflict
240
+ const { id: _ignoredId, ...sanitizedAttributes } = attributes || {};
241
+ // Extract relationship IDs from JSON:API relationships object
242
+ if (rels) {
243
+ for (const [key, value] of Object.entries(rels)) {
244
+ const relData = value?.data;
245
+ if (relData && relData.id !== undefined) {
246
+ sanitizedAttributes[key] = relData.id;
247
+ }
248
+ }
249
+ }
250
+ const recordAttributes = id !== undefined ? { id, ...sanitizedAttributes } : sanitizedAttributes;
251
+ const created = createRecord(model, recordAttributes, { serialize: false });
252
+ const record = isOrmRecord(created) ? created : null;
253
+ if (!record)
254
+ return 500;
255
+ return { data: record.toJSON?.({ fields: modelFields }) };
256
+ };
257
+ const updateHandler = async ({ body, params }) => {
258
+ const found = await store.find(model, getId(params));
259
+ if (!found || !isOrmRecord(found))
260
+ return 404;
261
+ const record = found;
262
+ const { attributes, relationships: rels } = (body?.data || {});
263
+ if (!attributes && !rels)
264
+ return 400; // Bad request
265
+ // Apply attribute updates 1 by 1 to utilize built-in transform logic, ignore id key
266
+ if (attributes) {
267
+ for (const [key, value] of Object.entries(attributes)) {
268
+ if (!Object.hasOwn(record, key))
269
+ continue;
270
+ if (key === 'id')
271
+ continue;
272
+ record[key] = value;
273
+ }
274
+ ;
275
+ }
276
+ // Apply relationship updates via updateRecord to properly resolve references
277
+ if (rels) {
278
+ const relUpdates = {};
279
+ for (const [key, value] of Object.entries(rels)) {
280
+ const relData = value?.data;
281
+ if (relData && relData.id !== undefined) {
282
+ relUpdates[key] = relData.id;
283
+ }
284
+ }
285
+ if (Object.keys(relUpdates).length > 0) {
286
+ updateRecord(record, relUpdates);
287
+ }
288
+ }
289
+ return { data: record.toJSON?.() };
290
+ };
291
+ const deleteHandler = ({ params }) => {
292
+ store.remove(model, getId(params));
293
+ return 204;
294
+ };
295
+ // Wrap handlers with hooks
296
+ const isView = Orm.instance?.isView?.(model);
297
+ this.handlers = {
298
+ get: {
299
+ '/': this._withHooks('list', getCollectionHandler),
300
+ '/:id': this._withHooks('get', getSingleHandler),
301
+ ...this._generateRelationshipRoutes(model, pluralizedModel, modelRelationships)
302
+ },
303
+ };
304
+ // Views are read-only -- no write endpoints
305
+ if (!isView) {
306
+ this.handlers.patch = {
307
+ '/:id': this._withHooks('update', updateHandler)
308
+ };
309
+ this.handlers.post = {
310
+ '/': this._withHooks('create', createHandler)
311
+ };
312
+ this.handlers.delete = {
313
+ '/:id': this._withHooks('delete', deleteHandler)
314
+ };
315
+ }
316
+ }
317
+ // Wraps a handler with before/after hook execution
318
+ _withHooks(operation, handler) {
319
+ return async (request, state) => {
320
+ // Build context object for hooks
321
+ const context = {
322
+ model: this.model,
323
+ operation,
324
+ request,
325
+ params: request.params,
326
+ body: request.body,
327
+ query: request.query,
328
+ state,
329
+ };
330
+ // Capture old state for operations that modify data
331
+ if (operation === 'update' || operation === 'delete') {
332
+ const existingRecord = await store.find(this.model, getId(request.params));
333
+ if (existingRecord) {
334
+ // Deep copy the record's data to preserve old state
335
+ context.oldState = JSON.parse(JSON.stringify(existingRecord.__data || existingRecord));
336
+ }
337
+ if (operation === 'delete') {
338
+ context.recordId = getId(request.params);
339
+ }
340
+ }
341
+ // Run before hooks sequentially (can halt by returning a value)
342
+ for (const hook of getBeforeHooks(operation, this.model)) {
343
+ const result = await hook(context);
344
+ if (result !== undefined) {
345
+ // Hook returned a value - halt operation and return result
346
+ return result;
347
+ }
348
+ }
349
+ // Execute main handler
350
+ const response = await handler(request, state);
351
+ // Persist to SQL database for write operations
352
+ const sqlDb = Orm.instance.sqlDb;
353
+ if (sqlDb && WRITE_OPERATIONS.has(operation)) {
354
+ await sqlDb.persist(operation, this.model, context, response);
355
+ }
356
+ // Add response and relevant records to context
357
+ context.response = response;
358
+ if (operation === 'get' && response?.data && !Array.isArray(response.data)) {
359
+ context.record = await store.find(this.model, getId(request.params));
360
+ }
361
+ else if (operation === 'list' && response?.data) {
362
+ context.records = await store.findAll(this.model);
363
+ }
364
+ else if (operation === 'create' && response?.data && (response.data.id)) {
365
+ // For create, get the record from store using the ID from the response
366
+ const responseData = response.data;
367
+ const recordId = isNaN(responseData.id) ? responseData.id : parseInt(responseData.id);
368
+ context.record = store.get(this.model, recordId);
369
+ }
370
+ else if (operation === 'update' && response?.data) {
371
+ context.record = store.get(this.model, getId(request.params));
372
+ }
373
+ else if (operation === 'delete') {
374
+ // For delete, the record may no longer exist, but we have oldState
375
+ context.recordId = getId(request.params);
376
+ }
377
+ // Run after hooks sequentially
378
+ for (const hook of getAfterHooks(operation, this.model)) {
379
+ await hook(context);
380
+ }
381
+ // Auto-save DB after write operations when configured
382
+ if (config.orm.db.autosave === 'onUpdate' && WRITE_OPERATIONS.has(operation)) {
383
+ await Orm.db.save();
384
+ }
385
+ return response;
386
+ };
387
+ }
388
+ _generateRelationshipRoutes(model, pluralizedModel, modelRelationships) {
389
+ const routes = {};
390
+ for (const [relationshipName, info] of Object.entries(modelRelationships)) {
391
+ // Dasherize the relationship name for URL paths (e.g., accessLinks -> access-links)
392
+ const dasherizedName = camelCaseToKebabCase(relationshipName);
393
+ // Related resource route: GET /:id/{relationship}
394
+ routes[`/:id/${dasherizedName}`] = async (request) => {
395
+ const record = await store.find(model, getId(request.params));
396
+ if (!record)
397
+ return 404;
398
+ const relatedData = record.__relationships[relationshipName];
399
+ const baseUrl = getBaseUrl(request);
400
+ let data;
401
+ if (info.isArray) {
402
+ // hasMany - return array
403
+ const related = Array.isArray(relatedData) ? relatedData.filter(isOrmRecord) : [];
404
+ data = related.map(r => r.toJSON?.({ baseUrl }));
405
+ }
406
+ else {
407
+ // belongsTo - return single or null
408
+ data = isOrmRecord(relatedData) ? relatedData.toJSON?.({ baseUrl }) : null;
409
+ }
410
+ return {
411
+ links: { self: `${baseUrl}/${pluralizedModel}/${request.params.id}/${dasherizedName}` },
412
+ data
413
+ };
414
+ };
415
+ // Relationship linkage route: GET /:id/relationships/{relationship}
416
+ routes[`/:id/relationships/${dasherizedName}`] = async (request) => {
417
+ const record = await store.find(model, getId(request.params));
418
+ if (!record)
419
+ return 404;
420
+ const relatedData = record.__relationships[relationshipName];
421
+ const baseUrl = getBaseUrl(request);
422
+ let data;
423
+ if (info.isArray) {
424
+ // hasMany - return array of linkage objects
425
+ const related = Array.isArray(relatedData) ? relatedData.filter(isOrmRecord) : [];
426
+ data = related
427
+ .filter((r) => Boolean(r.__model))
428
+ .map(r => ({ type: r.__model.__name, id: r.id }));
429
+ }
430
+ else {
431
+ // belongsTo - return single linkage or null
432
+ if (isOrmRecord(relatedData) && relatedData.__model) {
433
+ data = { type: relatedData.__model.__name, id: relatedData.id };
434
+ }
435
+ else {
436
+ data = null;
437
+ }
438
+ }
439
+ return {
440
+ links: {
441
+ self: `${baseUrl}/${pluralizedModel}/${request.params.id}/relationships/${dasherizedName}`,
442
+ related: `${baseUrl}/${pluralizedModel}/${request.params.id}/${dasherizedName}`
443
+ },
444
+ data
445
+ };
446
+ };
447
+ }
448
+ // Catch-all for invalid relationship names on related resource route
449
+ routes[`/:id/:relationship`] = async (request) => {
450
+ const record = await store.find(model, getId(request.params));
451
+ if (!record)
452
+ return 404;
453
+ // If we reach here, relationship doesn't exist (valid ones were registered above)
454
+ return 404;
455
+ };
456
+ // Catch-all for invalid relationship names on relationship linkage route
457
+ routes[`/:id/relationships/:relationship`] = async (request) => {
458
+ const record = await store.find(model, getId(request.params));
459
+ if (!record)
460
+ return 404;
461
+ return 404;
462
+ };
463
+ return routes;
464
+ }
465
+ auth(request, state) {
466
+ const access = this.access(request);
467
+ if (!access)
468
+ return 403;
469
+ if (Array.isArray(access) && !access.includes(methodAccessMap[request.method]))
470
+ return 403;
471
+ if (typeof access === 'function')
472
+ state.filter = access;
473
+ return undefined;
474
+ }
475
+ }
@@ -0,0 +1,4 @@
1
+ export declare function registerPluralName(modelName: string, modelClass: {
2
+ pluralName?: string;
3
+ }): void;
4
+ export declare function getPluralName(modelName: string): string;
@@ -0,0 +1,9 @@
1
+ import { pluralize } from './utils.js';
2
+ const registry = new Map();
3
+ export function registerPluralName(modelName, modelClass) {
4
+ const plural = modelClass.pluralName || pluralize(modelName);
5
+ registry.set(modelName, plural);
6
+ }
7
+ export function getPluralName(modelName) {
8
+ return registry.get(modelName) || pluralize(modelName);
9
+ }
@@ -0,0 +1,15 @@
1
+ import type { Pool as PgPool } from 'pg';
2
+ interface PgConfig {
3
+ host: string;
4
+ port: number;
5
+ user: string;
6
+ password: string;
7
+ database: string;
8
+ connectionLimit: number;
9
+ }
10
+ /**
11
+ * Create or return the singleton pg Pool.
12
+ */
13
+ export declare function getPool(pgConfig: PgConfig, extensions?: string[]): Promise<PgPool>;
14
+ export declare function closePool(): Promise<void>;
15
+ export {};
@@ -0,0 +1,32 @@
1
+ import { validateIdentifier } from './query-builder.js';
2
+ let pool = null;
3
+ /**
4
+ * Create or return the singleton pg Pool.
5
+ */
6
+ export async function getPool(pgConfig, extensions = ['vector']) {
7
+ if (pool)
8
+ return pool;
9
+ const { default: pg } = await import('pg');
10
+ pool = new pg.Pool({
11
+ host: pgConfig.host,
12
+ port: pgConfig.port,
13
+ user: pgConfig.user,
14
+ password: pgConfig.password,
15
+ database: pgConfig.database,
16
+ max: pgConfig.connectionLimit,
17
+ idleTimeoutMillis: 30000,
18
+ connectionTimeoutMillis: 10000,
19
+ });
20
+ // Enable requested PostgreSQL extensions
21
+ for (const ext of extensions) {
22
+ validateIdentifier(ext, 'extension name');
23
+ await pool.query(`CREATE EXTENSION IF NOT EXISTS "${ext}"`);
24
+ }
25
+ return pool;
26
+ }
27
+ export async function closePool() {
28
+ if (!pool)
29
+ return;
30
+ await pool.end();
31
+ pool = null;
32
+ }
@@ -0,0 +1,45 @@
1
+ import type { ForeignKeyDef, SnapshotEntry } from '../types/orm-types.js';
2
+ interface ColumnChange {
3
+ model: string;
4
+ column: string;
5
+ type: string;
6
+ }
7
+ interface ColumnTypeChange {
8
+ model: string;
9
+ column: string;
10
+ from: string;
11
+ to: string;
12
+ }
13
+ interface ForeignKeyChange {
14
+ model: string;
15
+ column: string;
16
+ references: ForeignKeyDef;
17
+ }
18
+ interface DiffResult {
19
+ hasChanges: boolean;
20
+ addedModels: string[];
21
+ removedModels: string[];
22
+ addedColumns: ColumnChange[];
23
+ removedColumns: ColumnChange[];
24
+ changedColumns: ColumnTypeChange[];
25
+ addedForeignKeys: ForeignKeyChange[];
26
+ removedForeignKeys: ForeignKeyChange[];
27
+ }
28
+ interface ViewDiffResult {
29
+ hasChanges: boolean;
30
+ addedViews: string[];
31
+ removedViews: string[];
32
+ changedViews: string[];
33
+ }
34
+ interface MigrationResult {
35
+ filename: string;
36
+ content: string;
37
+ snapshot: Record<string, unknown>;
38
+ }
39
+ export declare function generateMigration(description?: string, configKey?: string): Promise<MigrationResult | null>;
40
+ export declare function loadLatestSnapshot(migrationsPath: string): Promise<Record<string, unknown>>;
41
+ export declare function diffSnapshots(previous: Record<string, SnapshotEntry>, current: Record<string, SnapshotEntry>): DiffResult;
42
+ export declare function detectSchemaDrift(schemas: Record<string, unknown>, snapshot: Record<string, SnapshotEntry>): DiffResult;
43
+ export declare function extractViewsFromSnapshot(snapshot: Record<string, SnapshotEntry>): Record<string, SnapshotEntry>;
44
+ export declare function diffViewSnapshots(previous: Record<string, SnapshotEntry>, current: Record<string, SnapshotEntry>): ViewDiffResult;
45
+ export {};