@stonyx/orm 0.2.1-alpha.4 → 0.2.1-alpha.40

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