@stonyx/orm 0.2.5-alpha.0 → 0.3.1
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/README.md +482 -15
- package/config/environment.js +63 -6
- package/dist/aggregates.d.ts +21 -0
- package/dist/aggregates.js +93 -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 +180 -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 +75 -0
- package/dist/hooks.js +110 -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 +181 -0
- package/dist/manage-record.d.ts +13 -0
- package/dist/manage-record.js +123 -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 +254 -0
- package/dist/mysql/migration-runner.d.ts +12 -0
- package/dist/mysql/migration-runner.js +88 -0
- package/dist/mysql/mysql-db.d.ts +100 -0
- package/dist/mysql/mysql-db.js +425 -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 +257 -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 +475 -0
- package/dist/plural-registry.d.ts +4 -0
- package/dist/plural-registry.js +9 -0
- package/dist/postgres/connection.d.ts +15 -0
- package/dist/postgres/connection.js +32 -0
- package/dist/postgres/migration-generator.d.ts +45 -0
- package/dist/postgres/migration-generator.js +280 -0
- package/dist/postgres/migration-runner.d.ts +10 -0
- package/dist/postgres/migration-runner.js +87 -0
- package/dist/postgres/postgres-db.d.ts +119 -0
- package/dist/postgres/postgres-db.js +477 -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 +296 -0
- package/dist/postgres/type-map.d.ts +23 -0
- package/dist/postgres/type-map.js +56 -0
- package/dist/record.d.ts +75 -0
- package/dist/record.js +129 -0
- package/dist/relationships.d.ts +10 -0
- package/dist/relationships.js +41 -0
- package/dist/schema-helpers.d.ts +20 -0
- package/dist/schema-helpers.js +48 -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 +52 -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 +286 -0
- package/dist/timescale/query-builder.d.ts +43 -0
- package/dist/timescale/query-builder.js +115 -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 +153 -0
- package/dist/types/orm-types.js +1 -0
- package/dist/utils.d.ts +7 -0
- package/dist/utils.js +17 -0
- package/dist/view-resolver.d.ts +8 -0
- package/dist/view-resolver.js +171 -0
- package/dist/view.d.ts +11 -0
- package/dist/view.js +18 -0
- package/package.json +64 -11
- package/src/aggregates.ts +109 -0
- package/src/{attr.js → attr.ts} +2 -2
- package/src/belongs-to.ts +90 -0
- package/src/cli.ts +183 -0
- package/src/commands.ts +179 -0
- package/src/db.ts +232 -0
- package/src/exports/db.ts +7 -0
- package/src/has-many.ts +92 -0
- package/src/hooks.ts +151 -0
- package/src/{index.js → index.ts} +12 -2
- package/src/main.ts +229 -0
- package/src/manage-record.ts +161 -0
- package/src/{meta-request.js → meta-request.ts} +17 -14
- package/src/migrate.ts +72 -0
- package/src/model-property.ts +35 -0
- package/src/model.ts +21 -0
- package/src/mysql/connection.ts +43 -0
- package/src/mysql/migration-generator.ts +337 -0
- package/src/mysql/migration-runner.ts +121 -0
- package/src/mysql/mysql-db.ts +543 -0
- package/src/mysql/query-builder.ts +69 -0
- package/src/mysql/schema-introspector.ts +310 -0
- package/src/mysql/type-map.ts +42 -0
- package/src/orm-request.ts +582 -0
- package/src/plural-registry.ts +12 -0
- package/src/postgres/connection.ts +48 -0
- package/src/postgres/migration-generator.ts +370 -0
- package/src/postgres/migration-runner.ts +115 -0
- package/src/postgres/postgres-db.ts +616 -0
- package/src/postgres/query-builder.ts +148 -0
- package/src/postgres/schema-introspector.ts +360 -0
- package/src/postgres/type-map.ts +61 -0
- package/src/record.ts +186 -0
- package/src/relationships.ts +54 -0
- package/src/schema-helpers.ts +59 -0
- package/src/serializer.ts +161 -0
- package/src/setup-rest-server.ts +62 -0
- package/src/standalone-db.ts +185 -0
- package/src/store.ts +373 -0
- package/src/timescale/query-builder.ts +174 -0
- package/src/timescale/timescale-db.ts +119 -0
- package/src/transforms.ts +20 -0
- package/src/types/mysql2.d.ts +49 -0
- package/src/types/orm-types.ts +158 -0
- package/src/types/pg.d.ts +32 -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 +16 -0
- package/src/types/stonyx-utils.d.ts +33 -0
- package/src/types/stonyx.d.ts +21 -0
- package/src/utils.ts +22 -0
- package/src/view-resolver.ts +211 -0
- package/src/view.ts +22 -0
- package/.claude/project-structure.md +0 -578
- package/.github/workflows/ci.yml +0 -36
- package/.github/workflows/publish.yml +0 -143
- package/src/belongs-to.js +0 -63
- package/src/db.js +0 -80
- package/src/has-many.js +0 -61
- package/src/main.js +0 -119
- package/src/manage-record.js +0 -103
- package/src/model-property.js +0 -29
- package/src/model.js +0 -9
- package/src/orm-request.js +0 -249
- package/src/record.js +0 -100
- package/src/relationships.js +0 -43
- package/src/serializer.js +0 -138
- package/src/setup-rest-server.js +0 -57
- package/src/store.js +0 -211
- package/src/transforms.js +0 -20
- package/stonyx-bootstrap.cjs +0 -30
package/src/record.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { store } from '@stonyx/orm';
|
|
2
|
+
import { getComputedProperties } from "./serializer.js";
|
|
3
|
+
import { camelCaseToKebabCase } from '@stonyx/utils/string';
|
|
4
|
+
import { getPluralName } from './plural-registry.js';
|
|
5
|
+
import type Serializer from './serializer.js';
|
|
6
|
+
|
|
7
|
+
interface ToJSONOptions {
|
|
8
|
+
fields?: Set<string>;
|
|
9
|
+
baseUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface SerializeOptions {
|
|
13
|
+
update?: boolean;
|
|
14
|
+
serialize?: boolean;
|
|
15
|
+
transform?: boolean;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface UnloadOptions {
|
|
20
|
+
[key: string]: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface RelationshipLinks {
|
|
24
|
+
self: string;
|
|
25
|
+
related: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface RelationshipEntry {
|
|
29
|
+
data: { type: string; id: unknown } | { type: string; id: unknown }[] | null;
|
|
30
|
+
links?: RelationshipLinks;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface JSONAPIResult {
|
|
34
|
+
attributes: { [key: string]: unknown };
|
|
35
|
+
relationships: { [key: string]: RelationshipEntry };
|
|
36
|
+
id: unknown;
|
|
37
|
+
type: string;
|
|
38
|
+
links?: { self: string };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default class Record {
|
|
42
|
+
/** @private */
|
|
43
|
+
__data: { [key: string]: unknown } = {};
|
|
44
|
+
/** @private */
|
|
45
|
+
__relationships: { [key: string]: unknown } = {};
|
|
46
|
+
/** @private */
|
|
47
|
+
__serialized = false;
|
|
48
|
+
/** @private */
|
|
49
|
+
__model: { __name: string; [key: string]: unknown };
|
|
50
|
+
/** @private */
|
|
51
|
+
__serializer: Serializer;
|
|
52
|
+
|
|
53
|
+
[key: string]: unknown;
|
|
54
|
+
|
|
55
|
+
constructor(model: { __name: string; [key: string]: unknown }, serializer: Serializer) {
|
|
56
|
+
this.__model = model;
|
|
57
|
+
this.__serializer = serializer;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
serialize(rawData?: unknown, options: SerializeOptions = {}): { [key: string]: unknown } {
|
|
61
|
+
const { __data: data } = this;
|
|
62
|
+
|
|
63
|
+
if (this.__serialized && !options.update) {
|
|
64
|
+
const relatedIds: { [key: string]: unknown } = {};
|
|
65
|
+
|
|
66
|
+
for (const [key, childRecord] of Object.entries(this.__relationships)) {
|
|
67
|
+
relatedIds[key] = Array.isArray(childRecord)
|
|
68
|
+
? childRecord.map((r: Record) => r.id)
|
|
69
|
+
: (childRecord as Record)?.id ?? null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { ...data, ...relatedIds };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const normalizedData = this.__serializer.normalize(rawData);
|
|
76
|
+
this.__serializer.setProperties(normalizedData, this, options);
|
|
77
|
+
|
|
78
|
+
return data;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Similar to serialize, but preserves top level relationship records
|
|
82
|
+
format(): { [key: string]: unknown } {
|
|
83
|
+
if (!this.__serialized) throw new Error('Record must be serialized before being converted to JSON');
|
|
84
|
+
|
|
85
|
+
const { __data: data } = this;
|
|
86
|
+
const records: { [key: string]: unknown } = {};
|
|
87
|
+
|
|
88
|
+
for (const [key, childRecord] of Object.entries(this.__relationships)) {
|
|
89
|
+
if (Array.isArray(childRecord)) {
|
|
90
|
+
// Deduplicate by record ID — keep last occurrence (latest data wins)
|
|
91
|
+
const seen = new Set<unknown>();
|
|
92
|
+
const unique: Record[] = [];
|
|
93
|
+
|
|
94
|
+
for (let i = childRecord.length - 1; i >= 0; i--) {
|
|
95
|
+
const r = childRecord[i] as Record;
|
|
96
|
+
if (!seen.has(r.id)) {
|
|
97
|
+
seen.add(r.id);
|
|
98
|
+
unique.push(r);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
unique.reverse();
|
|
103
|
+
records[key] = unique.map((r: Record) => r.serialize());
|
|
104
|
+
} else {
|
|
105
|
+
records[key] = (childRecord as Record)?.serialize() ?? null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { ...data, ...records };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Formats record for JSON API output
|
|
113
|
+
toJSON(options: ToJSONOptions = {}): JSONAPIResult {
|
|
114
|
+
if (!this.__serialized) throw new Error('Record must be serialized before being converted to JSON');
|
|
115
|
+
|
|
116
|
+
const { fields, baseUrl } = options;
|
|
117
|
+
const { __data: data } = this;
|
|
118
|
+
const modelName = this.__model.__name;
|
|
119
|
+
const pluralizedModelName = getPluralName(modelName);
|
|
120
|
+
const recordId = data.id;
|
|
121
|
+
const relationships: { [key: string]: RelationshipEntry } = {};
|
|
122
|
+
const attributes: { [key: string]: unknown } = {};
|
|
123
|
+
|
|
124
|
+
for (const [key, value] of Object.entries(data)) {
|
|
125
|
+
if (key === 'id') continue;
|
|
126
|
+
if (fields && !fields.has(key)) continue;
|
|
127
|
+
attributes[key] = value;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const [key, getter] of getComputedProperties(this.__model)) {
|
|
131
|
+
if (fields && !fields.has(key)) continue;
|
|
132
|
+
attributes[key] = (getter as () => unknown).call(this);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const [key, childRecord] of Object.entries(this.__relationships)) {
|
|
136
|
+
if (fields && !fields.has(key)) continue;
|
|
137
|
+
|
|
138
|
+
const relationshipData = Array.isArray(childRecord)
|
|
139
|
+
? childRecord.map((r: Record) => ({ type: r.__model.__name, id: r.id }))
|
|
140
|
+
: childRecord ? { type: (childRecord as Record).__model.__name, id: (childRecord as Record).id } : null;
|
|
141
|
+
|
|
142
|
+
// Dasherize the key for URL paths (e.g., accessLinks -> access-links)
|
|
143
|
+
const dasherizedKey = camelCaseToKebabCase(key);
|
|
144
|
+
|
|
145
|
+
relationships[dasherizedKey] = { data: relationshipData };
|
|
146
|
+
|
|
147
|
+
// Add links to relationship if baseUrl provided
|
|
148
|
+
if (baseUrl) {
|
|
149
|
+
relationships[dasherizedKey].links = {
|
|
150
|
+
self: `${baseUrl}/${pluralizedModelName}/${recordId}/relationships/${dasherizedKey}`,
|
|
151
|
+
related: `${baseUrl}/${pluralizedModelName}/${recordId}/${dasherizedKey}`
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const result: JSONAPIResult = {
|
|
157
|
+
attributes,
|
|
158
|
+
relationships,
|
|
159
|
+
id: recordId,
|
|
160
|
+
type: modelName,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Add resource links if baseUrl provided
|
|
164
|
+
if (baseUrl) {
|
|
165
|
+
result.links = {
|
|
166
|
+
self: `${baseUrl}/${pluralizedModelName}/${recordId}`
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
unload(options: UnloadOptions = {}): void {
|
|
174
|
+
store.unloadRecord(this.__model.__name, this.id, options);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
clean(): void {
|
|
178
|
+
try {
|
|
179
|
+
for (const key of Object.keys(this)) {
|
|
180
|
+
delete this[key];
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
// Ignore errors during cleanup, as some keys may not be deletable
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { relationships } from '@stonyx/orm';
|
|
2
|
+
import type { HasManyMap, BelongsToMap, GlobalMap, PendingMap, PendingBelongsToMap } from './types/orm-types.js';
|
|
3
|
+
|
|
4
|
+
// TODO: Refactor mapping to remove a level of iteration
|
|
5
|
+
export function getRelationships(type: string, sourceModel: string, targetModel: string, relationshipId?: string): Map<unknown, unknown> | undefined {
|
|
6
|
+
let allRelationships = relationships.get(type) as Map<string, Map<string, Map<unknown, unknown>>> | undefined;
|
|
7
|
+
|
|
8
|
+
if (!allRelationships) {
|
|
9
|
+
allRelationships = new Map();
|
|
10
|
+
relationships.set(type, allRelationships);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// create relationship map for this type of it doesn't already exist
|
|
14
|
+
if (!allRelationships.has(sourceModel)) allRelationships.set(sourceModel, new Map());
|
|
15
|
+
|
|
16
|
+
const modelRelationship = allRelationships.get(sourceModel) as Map<string, Map<unknown, unknown>> | undefined;
|
|
17
|
+
if (!modelRelationship) return undefined;
|
|
18
|
+
|
|
19
|
+
if (!modelRelationship.has(targetModel)) modelRelationship.set(targetModel, new Map());
|
|
20
|
+
|
|
21
|
+
const relationship = modelRelationship.get(targetModel) as Map<unknown, unknown> | undefined;
|
|
22
|
+
|
|
23
|
+
// TODO: Determine whether already having id should be handled differently
|
|
24
|
+
//if (relationship.has(relationshipId)) return;
|
|
25
|
+
|
|
26
|
+
return relationship;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getHasManyRelationships(sourceModel: string, targetModel: string): Map<unknown, unknown> | undefined {
|
|
30
|
+
return (relationships.get('hasMany') as HasManyMap | undefined)?.get(sourceModel)?.get(targetModel);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Typed accessors for the relationship registry */
|
|
34
|
+
export function getHasManyRegistry(): HasManyMap {
|
|
35
|
+
return relationships.get('hasMany') as HasManyMap;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getBelongsToRegistry(): BelongsToMap {
|
|
39
|
+
return relationships.get('belongsTo') as BelongsToMap;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getGlobalRegistry(): GlobalMap {
|
|
43
|
+
return relationships.get('global') as GlobalMap;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getPendingRegistry(): PendingMap {
|
|
47
|
+
return relationships.get('pending') as PendingMap;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getPendingBelongsToRegistry(): PendingBelongsToMap {
|
|
51
|
+
return relationships.get('pendingBelongsTo') as PendingBelongsToMap;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const TYPES: string[] = ['global', 'hasMany', 'belongsTo', 'pending'];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ModelSchema } from './types/orm-types.js';
|
|
2
|
+
|
|
3
|
+
export interface SchemaRelationshipInfo {
|
|
4
|
+
type: 'belongsTo' | 'hasMany';
|
|
5
|
+
modelName: string | null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Detect relationship type and target model from a model property function.
|
|
10
|
+
* Returns null if the property is not a relationship.
|
|
11
|
+
*/
|
|
12
|
+
export function getRelationshipInfo(property: unknown): SchemaRelationshipInfo | null {
|
|
13
|
+
if (typeof property !== 'function') return null;
|
|
14
|
+
const relType = (property as { __relationshipType?: string }).__relationshipType;
|
|
15
|
+
const modelName = (property as { __relatedModelName?: string }).__relatedModelName || null;
|
|
16
|
+
|
|
17
|
+
if (relType === 'belongsTo') return { type: 'belongsTo', modelName };
|
|
18
|
+
if (relType === 'hasMany') return { type: 'hasMany', modelName };
|
|
19
|
+
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Sanitize a model/table name for use in SQL identifiers.
|
|
25
|
+
* Replaces hyphens and slashes with underscores.
|
|
26
|
+
*/
|
|
27
|
+
export function sanitizeTableName(name: string): string {
|
|
28
|
+
return name.replace(/[-/]/g, '_');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Sort model schemas in dependency order (belongsTo targets before dependents).
|
|
33
|
+
* Uses depth-first traversal to ensure referenced tables are created first.
|
|
34
|
+
*/
|
|
35
|
+
export function getTopologicalOrder(schemas: Record<string, ModelSchema>): string[] {
|
|
36
|
+
const visited = new Set<string>();
|
|
37
|
+
const order: string[] = [];
|
|
38
|
+
|
|
39
|
+
function visit(name: string): void {
|
|
40
|
+
if (visited.has(name)) return;
|
|
41
|
+
visited.add(name);
|
|
42
|
+
|
|
43
|
+
const schema = schemas[name];
|
|
44
|
+
if (!schema) return;
|
|
45
|
+
|
|
46
|
+
// Visit dependencies (belongsTo targets) first
|
|
47
|
+
for (const targetModelName of Object.values(schema.relationships.belongsTo)) {
|
|
48
|
+
if (targetModelName) visit(targetModelName);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
order.push(name);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const name of Object.keys(schemas)) {
|
|
55
|
+
visit(name);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return order;
|
|
59
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import config from 'stonyx/config';
|
|
2
|
+
import { get, makeArray } from '@stonyx/utils/object';
|
|
3
|
+
import type { AggregateProperty } from './aggregates.js';
|
|
4
|
+
import type ModelProperty from './model-property.js';
|
|
5
|
+
|
|
6
|
+
const RESERVED_KEYS = ['__name'];
|
|
7
|
+
|
|
8
|
+
function isAggregateProperty(v: unknown): v is AggregateProperty {
|
|
9
|
+
return typeof v === 'object' && v !== null && (v as { __kind?: string }).__kind === 'AggregateProperty';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isModelProperty(v: unknown): v is ModelProperty {
|
|
13
|
+
return typeof v === 'object' && v !== null && (v as { __kind?: string }).__kind === 'ModelProperty';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function searchQuery(query: Record<string, unknown>, array: unknown, key?: string): unknown {
|
|
17
|
+
const result = makeArray(array).find((item: unknown) => {
|
|
18
|
+
for (const [prop, value] of Object.entries(query)) {
|
|
19
|
+
if ((item as Record<string, unknown>)[prop] !== value) return false;
|
|
20
|
+
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (!result) return null;
|
|
26
|
+
if (key) return (result as Record<string, unknown>)[key];
|
|
27
|
+
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function query(rawData: unknown, pathPrefix: string, subPath: unknown): unknown {
|
|
32
|
+
if (!rawData) return null;
|
|
33
|
+
|
|
34
|
+
const [path, getter, pointer] = makeArray(subPath) as [string, unknown, string | undefined];
|
|
35
|
+
const fullPath = `${pathPrefix}${path}`;
|
|
36
|
+
const value = get(rawData as Record<string, unknown>, fullPath);
|
|
37
|
+
|
|
38
|
+
if (getter === undefined || getter === null) return value;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
switch (typeof getter) {
|
|
42
|
+
case 'object':
|
|
43
|
+
return searchQuery(getter as Record<string, unknown>, value, pointer);
|
|
44
|
+
|
|
45
|
+
case 'function':
|
|
46
|
+
return (getter as (v: unknown) => unknown)(value);
|
|
47
|
+
|
|
48
|
+
case 'number': {
|
|
49
|
+
const element = (value as unknown[])[getter];
|
|
50
|
+
return pointer ? (element as Record<string, unknown>)[pointer] : element;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
default:
|
|
54
|
+
return (value as Record<string, unknown>)[getter as string];
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (config.debug) console.error(`Cannot parse value for ${fullPath}.`, { getter, query }, error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default class Serializer {
|
|
62
|
+
map: Record<string, unknown> = {};
|
|
63
|
+
path = '';
|
|
64
|
+
model: Record<string, unknown>;
|
|
65
|
+
|
|
66
|
+
constructor(model: Record<string, unknown>) {
|
|
67
|
+
this.model = model;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* This method populates the record's instance with instances of
|
|
72
|
+
* the ModelProperty object, while setting parsed values to the record's
|
|
73
|
+
* __data property, which represents the serialized version of the data
|
|
74
|
+
*/
|
|
75
|
+
setProperties(rawData: unknown, record: unknown, options: Record<string, unknown>): void {
|
|
76
|
+
const { path, model } = this;
|
|
77
|
+
const keys = Object.keys(model).filter(key => !RESERVED_KEYS.includes(key));
|
|
78
|
+
const pathPrefix = path ? `${path}.` : '';
|
|
79
|
+
const rec = record as Record<string, unknown>;
|
|
80
|
+
const parsedData = rec.__data as Record<string, unknown>;
|
|
81
|
+
const relatedRecords = rec.__relationships as Record<string, unknown>;
|
|
82
|
+
|
|
83
|
+
for (const key of keys) {
|
|
84
|
+
const subPath = options.serialize ? ((this.map as Record<string, unknown>)[key] || key) : key;
|
|
85
|
+
const handler = model[key];
|
|
86
|
+
const data = query(rawData, pathPrefix, subPath);
|
|
87
|
+
|
|
88
|
+
// Skip fields not present in the update payload (undefined = not provided)
|
|
89
|
+
if (data === undefined && options.update) continue;
|
|
90
|
+
|
|
91
|
+
// Relationship handling
|
|
92
|
+
if (typeof handler === 'function') {
|
|
93
|
+
// Pass relationship key name to handler for pending fulfillment
|
|
94
|
+
const handlerOptions = { ...options, _relationshipKey: key };
|
|
95
|
+
const childRecord = handler(record, data, handlerOptions);
|
|
96
|
+
|
|
97
|
+
rec[key] = childRecord;
|
|
98
|
+
relatedRecords[key] = childRecord;
|
|
99
|
+
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Aggregate property handling — use the rawData value, not the aggregate descriptor
|
|
104
|
+
if (isAggregateProperty(handler)) {
|
|
105
|
+
parsedData[key] = data;
|
|
106
|
+
rec[key] = data;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Direct assignment handling
|
|
111
|
+
if (!isModelProperty(handler)) {
|
|
112
|
+
parsedData[key] = handler;
|
|
113
|
+
rec[key] = handler;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const prop = handler as { value: unknown; ignoreFirstTransform: boolean };
|
|
118
|
+
|
|
119
|
+
Object.defineProperty(record, key, {
|
|
120
|
+
enumerable: true,
|
|
121
|
+
configurable: true,
|
|
122
|
+
get: () => prop.value,
|
|
123
|
+
set(newValue: unknown) {
|
|
124
|
+
prop.ignoreFirstTransform = !options.transform;
|
|
125
|
+
prop.value = newValue;
|
|
126
|
+
parsedData[key] = prop.value;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
rec[key] = data;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (options.update) return;
|
|
134
|
+
|
|
135
|
+
// Serialize computed properties
|
|
136
|
+
for (const [key, getter] of getComputedProperties(this.model)) {
|
|
137
|
+
Object.defineProperty(record, key, {
|
|
138
|
+
enumerable: true,
|
|
139
|
+
get: () => (getter as () => unknown).call(record)
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
rec.__serialized = true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* OVERRIDE: This hook allows for data manipulation prior to serialization logic
|
|
148
|
+
*/
|
|
149
|
+
normalize(data: unknown): unknown {
|
|
150
|
+
return data;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function getComputedProperties(classInstance: Record<string, unknown>): [string, PropertyDescriptor['get']][] {
|
|
155
|
+
const proto = Object.getPrototypeOf(classInstance);
|
|
156
|
+
if (!proto || proto === Object.prototype) return [];
|
|
157
|
+
|
|
158
|
+
return Object.entries(Object.getOwnPropertyDescriptors(proto))
|
|
159
|
+
.filter(([key, descriptor]) => key !== 'constructor' && descriptor.get)
|
|
160
|
+
.map(([key, descriptor]) => [key, descriptor.get]);
|
|
161
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { waitForModule } from 'stonyx';
|
|
2
|
+
import { store } from '@stonyx/orm';
|
|
3
|
+
import OrmRequest from './orm-request.js';
|
|
4
|
+
import MetaRequest from './meta-request.js';
|
|
5
|
+
import RestServer from '@stonyx/rest-server';
|
|
6
|
+
import { forEachFileImport } from '@stonyx/utils/file';
|
|
7
|
+
import { dbKey } from './db.js';
|
|
8
|
+
import { getPluralName } from './plural-registry.js';
|
|
9
|
+
import log from 'stonyx/log';
|
|
10
|
+
|
|
11
|
+
interface AccessInstance {
|
|
12
|
+
models: string[] | '*';
|
|
13
|
+
access: (request: unknown) => unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default async function(route: string, accessPath: string, metaRoute: boolean): Promise<void> {
|
|
17
|
+
const accessFiles: Record<string, (request: unknown) => unknown> = {};
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
await forEachFileImport(accessPath, (accessClass: unknown) => {
|
|
21
|
+
const accessInstance = new (accessClass as new () => AccessInstance)();
|
|
22
|
+
const { models } = accessInstance;
|
|
23
|
+
|
|
24
|
+
if (!models) throw new Error(`Access class "${(accessClass as { name: string }).name}" must define a "models" list`);
|
|
25
|
+
|
|
26
|
+
if (models.length === 0) return; // No models to assign access to
|
|
27
|
+
if (typeof accessInstance.access !== 'function') throw new Error(`Access class "${(accessClass as { name: string }).name}" must declare an "access" method`);
|
|
28
|
+
|
|
29
|
+
const availableModels = Array.from(store.data.keys());
|
|
30
|
+
|
|
31
|
+
for (const model of models === '*' ? availableModels : models) {
|
|
32
|
+
if (model === dbKey) continue;
|
|
33
|
+
if (!store.data.has(model)) throw new Error(`Unable to define access for Invalid Model "${model}". Model does not exist`);
|
|
34
|
+
if (accessFiles![model]) throw new Error(`Access for model "${model}" has already been defined by another access class.`);
|
|
35
|
+
|
|
36
|
+
accessFiles![model] = accessInstance.access;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
} catch (error) {
|
|
40
|
+
log.error?.(error instanceof Error ? error.message : String(error));
|
|
41
|
+
log.warn?.('You must define a valid access configuration file in order to access ORM generated REST endpoints.');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await waitForModule('rest-server');
|
|
45
|
+
|
|
46
|
+
// Remove "/" prefix and name mount point accordingly
|
|
47
|
+
const name = route === '/' ? 'index' : (route[0] === '/' ? route.slice(1) : route);
|
|
48
|
+
|
|
49
|
+
// Configure endpoints for models and views with access configuration
|
|
50
|
+
for (const [model, access] of Object.entries(accessFiles!)) {
|
|
51
|
+
const pluralizedModel = getPluralName(model);
|
|
52
|
+
const modelName = name === 'index' ? pluralizedModel : `${name}/${pluralizedModel}`;
|
|
53
|
+
RestServer.instance.mountRoute(OrmRequest, { name: modelName, options: { model, access } });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Mount the meta route when metaRoute config is enabled
|
|
57
|
+
if (metaRoute) {
|
|
58
|
+
log.warn?.('SECURITY RISK! - Meta route is enabled via metaRoute config. This feature is intended for development purposes only!');
|
|
59
|
+
|
|
60
|
+
RestServer.instance.mountRoute(MetaRequest, { name });
|
|
61
|
+
}
|
|
62
|
+
}
|