@stonyx/orm 0.2.1-beta.83 → 0.2.1-beta.85

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