@stonyx/orm 0.2.1-beta.9 → 0.2.1-beta.90

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