@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.
- package/config/environment.js +17 -0
- 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 +58 -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 +57 -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 +178 -0
- package/dist/manage-record.d.ts +13 -0
- package/dist/manage-record.js +113 -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 +411 -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 +453 -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 +473 -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 +35 -0
- package/dist/serializer.d.ts +17 -0
- package/dist/serializer.js +130 -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 +44 -0
- package/dist/timescale/timescale-db.js +81 -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 +165 -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.js → belongs-to.ts} +36 -17
- 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 +91 -0
- package/src/{hooks.js → hooks.ts} +23 -27
- package/src/{index.js → index.ts} +4 -4
- package/src/{main.js → main.ts} +64 -34
- package/src/{manage-record.js → manage-record.ts} +41 -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} +533 -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} +165 -95
- package/src/plural-registry.ts +12 -0
- package/src/postgres/connection.ts +46 -0
- 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} +199 -111
- 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 +48 -0
- package/src/{serializer.js → serializer.ts} +44 -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.ts +137 -0
- package/src/timescale/timescale-db.ts +107 -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} +53 -28
- package/src/view.ts +22 -0
- package/src/has-many.js +0 -68
- package/src/postgres/connection.js +0 -30
- package/src/relationships.js +0 -43
- 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,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
|
|
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)
|
|
123
|
-
seen.get(type)
|
|
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"
|
|
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
|
-
|
|
214
|
-
|
|
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 (!
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
426
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|