@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.
- package/dist/aggregates.d.ts +21 -0
- package/dist/aggregates.js +90 -0
- package/dist/attr.d.ts +2 -0
- package/dist/attr.js +22 -0
- package/dist/belongs-to.d.ts +11 -0
- package/dist/belongs-to.js +59 -0
- package/dist/cli.d.ts +22 -0
- package/dist/cli.js +148 -0
- package/dist/commands.d.ts +7 -0
- package/dist/commands.js +146 -0
- package/dist/db.d.ts +21 -0
- package/dist/db.js +174 -0
- package/dist/exports/db.d.ts +7 -0
- package/{src → dist}/exports/db.js +2 -4
- package/dist/has-many.d.ts +11 -0
- package/dist/has-many.js +58 -0
- package/dist/hooks.d.ts +47 -0
- package/dist/hooks.js +106 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +34 -0
- package/dist/main.d.ts +46 -0
- package/dist/main.js +179 -0
- package/dist/manage-record.d.ts +13 -0
- package/dist/manage-record.js +114 -0
- package/dist/meta-request.d.ts +6 -0
- package/dist/meta-request.js +52 -0
- package/dist/migrate.d.ts +2 -0
- package/dist/migrate.js +57 -0
- package/dist/model-property.d.ts +9 -0
- package/dist/model-property.js +29 -0
- package/dist/model.d.ts +15 -0
- package/dist/model.js +18 -0
- package/dist/mysql/connection.d.ts +14 -0
- package/dist/mysql/connection.js +24 -0
- package/dist/mysql/migration-generator.d.ts +45 -0
- package/dist/mysql/migration-generator.js +245 -0
- package/dist/mysql/migration-runner.d.ts +12 -0
- package/dist/mysql/migration-runner.js +83 -0
- package/dist/mysql/mysql-db.d.ts +100 -0
- package/dist/mysql/mysql-db.js +415 -0
- package/dist/mysql/query-builder.d.ts +10 -0
- package/dist/mysql/query-builder.js +44 -0
- package/dist/mysql/schema-introspector.d.ts +19 -0
- package/dist/mysql/schema-introspector.js +286 -0
- package/dist/mysql/type-map.d.ts +21 -0
- package/dist/mysql/type-map.js +36 -0
- package/dist/orm-request.d.ts +38 -0
- package/dist/orm-request.js +455 -0
- package/dist/plural-registry.d.ts +4 -0
- package/{src → dist}/plural-registry.js +3 -6
- package/dist/postgres/connection.d.ts +15 -0
- package/dist/postgres/connection.js +30 -0
- package/dist/postgres/migration-generator.d.ts +45 -0
- package/dist/postgres/migration-generator.js +257 -0
- package/dist/postgres/migration-runner.d.ts +10 -0
- package/dist/postgres/migration-runner.js +82 -0
- package/dist/postgres/postgres-db.d.ts +119 -0
- package/dist/postgres/postgres-db.js +476 -0
- package/dist/postgres/query-builder.d.ts +27 -0
- package/dist/postgres/query-builder.js +98 -0
- package/dist/postgres/schema-introspector.d.ts +29 -0
- package/dist/postgres/schema-introspector.js +309 -0
- package/dist/postgres/type-map.d.ts +23 -0
- package/dist/postgres/type-map.js +53 -0
- package/dist/record.d.ts +75 -0
- package/dist/record.js +115 -0
- package/dist/relationships.d.ts +10 -0
- package/dist/relationships.js +39 -0
- package/dist/serializer.d.ts +17 -0
- package/dist/serializer.js +136 -0
- package/dist/setup-rest-server.d.ts +1 -0
- package/dist/setup-rest-server.js +54 -0
- package/dist/standalone-db.d.ts +58 -0
- package/dist/standalone-db.js +142 -0
- package/dist/store.d.ts +62 -0
- package/dist/store.js +271 -0
- package/dist/timescale/query-builder.d.ts +41 -0
- package/dist/timescale/query-builder.js +87 -0
- package/dist/timescale/timescale-db.d.ts +45 -0
- package/dist/timescale/timescale-db.js +84 -0
- package/dist/transforms.d.ts +2 -0
- package/dist/transforms.js +17 -0
- package/dist/types/orm-types.d.ts +142 -0
- package/dist/types/orm-types.js +1 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +13 -0
- package/dist/view-resolver.d.ts +8 -0
- package/dist/view-resolver.js +169 -0
- package/dist/view.d.ts +11 -0
- package/dist/view.js +18 -0
- package/package.json +34 -11
- package/src/{aggregates.js → aggregates.ts} +27 -13
- package/src/{attr.js → attr.ts} +2 -2
- package/src/belongs-to.ts +90 -0
- package/src/{cli.js → cli.ts} +17 -11
- package/src/{commands.js → commands.ts} +179 -170
- package/src/{db.js → db.ts} +35 -26
- package/src/exports/db.ts +7 -0
- package/src/has-many.ts +92 -0
- package/src/{hooks.js → hooks.ts} +23 -27
- package/src/{index.js → index.ts} +4 -4
- package/src/{main.js → main.ts} +60 -34
- package/src/{manage-record.js → manage-record.ts} +42 -22
- package/src/{meta-request.js → meta-request.ts} +17 -14
- package/src/{migrate.js → migrate.ts} +9 -9
- package/src/{model-property.js → model-property.ts} +12 -6
- package/src/{model.js → model.ts} +5 -4
- package/src/mysql/{connection.js → connection.ts} +43 -28
- package/src/mysql/{migration-generator.js → migration-generator.ts} +332 -286
- package/src/mysql/{migration-runner.js → migration-runner.ts} +116 -110
- package/src/mysql/{mysql-db.js → mysql-db.ts} +537 -473
- package/src/mysql/{query-builder.js → query-builder.ts} +69 -64
- package/src/mysql/{schema-introspector.js → schema-introspector.ts} +355 -325
- package/src/mysql/{type-map.js → type-map.ts} +42 -37
- package/src/{orm-request.js → orm-request.ts} +169 -97
- package/src/plural-registry.ts +12 -0
- package/src/postgres/{connection.js → connection.ts} +14 -5
- package/src/postgres/{migration-generator.js → migration-generator.ts} +82 -38
- package/src/postgres/{migration-runner.js → migration-runner.ts} +11 -10
- package/src/postgres/{postgres-db.js → postgres-db.ts} +198 -114
- package/src/postgres/{query-builder.js → query-builder.ts} +27 -28
- package/src/postgres/{schema-introspector.js → schema-introspector.ts} +87 -58
- package/src/postgres/{type-map.js → type-map.ts} +10 -6
- package/src/{record.js → record.ts} +73 -34
- package/src/relationships.ts +53 -0
- package/src/{serializer.js → serializer.ts} +52 -36
- package/src/{setup-rest-server.js → setup-rest-server.ts} +18 -13
- package/src/{standalone-db.js → standalone-db.ts} +33 -24
- package/src/{store.js → store.ts} +90 -68
- package/src/timescale/{query-builder.js → query-builder.ts} +33 -38
- package/src/timescale/timescale-db.ts +119 -0
- package/src/transforms.ts +20 -0
- package/src/types/mysql2.d.ts +30 -0
- package/src/types/orm-types.ts +146 -0
- package/src/types/pg.d.ts +28 -0
- package/src/types/stonyx-cron.d.ts +5 -0
- package/src/types/stonyx-events.d.ts +4 -0
- package/src/types/stonyx-rest-server.d.ts +11 -0
- package/src/types/stonyx-utils.d.ts +33 -0
- package/src/types/stonyx.d.ts +21 -0
- package/src/utils.ts +16 -0
- package/src/{view-resolver.js → view-resolver.ts} +51 -24
- package/src/view.ts +22 -0
- package/src/belongs-to.js +0 -70
- package/src/has-many.js +0 -68
- package/src/relationships.js +0 -43
- package/src/timescale/timescale-db.js +0 -111
- package/src/transforms.js +0 -20
- package/src/utils.js +0 -12
- package/src/view.js +0 -21
|
@@ -1,37 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
string
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
21
|
-
if (
|
|
65
|
+
const relType = (property as { __relationshipType?: string }).__relationshipType;
|
|
66
|
+
if (relType === 'belongsTo') {
|
|
22
67
|
return { type: 'belongsTo', isArray: false };
|
|
23
68
|
}
|
|
24
|
-
if (
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
118
|
-
|
|
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 (!
|
|
123
|
-
|
|
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"
|
|
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
|
-
|
|
214
|
-
|
|
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 (!
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
426
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|