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