@stonyx/orm 0.2.1-beta.82 → 0.2.1-beta.84

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