@stonyx/orm 0.2.1-alpha.4 → 0.2.1-alpha.40
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 +64 -6
- package/config/environment.js +37 -1
- 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 +47 -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 +291 -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 +474 -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 +261 -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 +314 -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/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 +142 -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 +57 -15
- 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.js → commands.ts} +179 -170
- package/src/{db.js → db.ts} +55 -29
- package/src/exports/db.ts +7 -0
- package/src/has-many.ts +92 -0
- package/src/{hooks.js → hooks.ts} +25 -27
- package/src/{index.js → index.ts} +8 -5
- 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.js → migrate.ts} +9 -9
- package/src/model-property.ts +35 -0
- package/src/model.ts +21 -0
- package/src/mysql/{connection.js → connection.ts} +43 -28
- package/src/mysql/migration-generator.ts +337 -0
- package/src/mysql/{migration-runner.js → migration-runner.ts} +121 -110
- package/src/mysql/mysql-db.ts +543 -0
- package/src/mysql/{query-builder.js → query-builder.ts} +69 -64
- package/src/mysql/schema-introspector.ts +358 -0
- package/src/mysql/{type-map.js → type-map.ts} +42 -37
- package/src/{orm-request.js → orm-request.ts} +196 -103
- package/src/plural-registry.ts +12 -0
- package/src/postgres/connection.ts +48 -0
- package/src/postgres/migration-generator.ts +348 -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 +386 -0
- package/src/postgres/type-map.ts +61 -0
- package/src/record.ts +186 -0
- package/src/relationships.ts +54 -0
- package/src/serializer.ts +161 -0
- package/src/{setup-rest-server.js → setup-rest-server.ts} +18 -16
- 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 +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 +22 -0
- package/src/view-resolver.ts +211 -0
- package/src/view.ts +22 -0
- package/.claude/code-style-rules.md +0 -44
- package/.claude/hooks.md +0 -250
- package/.claude/index.md +0 -279
- package/.claude/usage-patterns.md +0 -217
- package/.github/workflows/ci.yml +0 -16
- package/.github/workflows/publish.yml +0 -51
- package/improvements.md +0 -139
- package/project-structure.md +0 -343
- package/src/belongs-to.js +0 -63
- package/src/has-many.js +0 -61
- package/src/main.js +0 -159
- package/src/manage-record.js +0 -118
- package/src/model-property.js +0 -29
- package/src/model.js +0 -19
- package/src/mysql/migration-generator.js +0 -188
- package/src/mysql/mysql-db.js +0 -422
- package/src/mysql/schema-introspector.js +0 -159
- package/src/record.js +0 -127
- package/src/relationships.js +0 -43
- package/src/serializer.js +0 -138
- package/src/store.js +0 -316
- package/src/transforms.js +0 -20
- package/src/utils.js +0 -12
- package/test-events-setup.js +0 -41
- package/test-hooks-manual.js +0 -54
- package/test-hooks-with-logging.js +0 -52
package/src/{db.js → db.ts}
RENAMED
|
@@ -17,20 +17,36 @@
|
|
|
17
17
|
import Cron from '@stonyx/cron';
|
|
18
18
|
import config from 'stonyx/config';
|
|
19
19
|
import log from 'stonyx/log';
|
|
20
|
-
import Orm, {
|
|
20
|
+
import Orm, { store } from '@stonyx/orm';
|
|
21
|
+
import { createRecord } from './manage-record.js';
|
|
21
22
|
import { createFile, createDirectory, updateFile, readFile, fileExists } from '@stonyx/utils/file';
|
|
22
23
|
import path from 'path';
|
|
23
24
|
|
|
24
25
|
export const dbKey = '__db';
|
|
25
26
|
|
|
27
|
+
interface DBRecord {
|
|
28
|
+
format(): Record<string, unknown>;
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function asDBRecord(value: unknown): DBRecord {
|
|
33
|
+
if (typeof value !== 'object' || value === null || typeof (value as DBRecord).format !== 'function') {
|
|
34
|
+
throw new Error('createRecord did not return a valid DBRecord');
|
|
35
|
+
}
|
|
36
|
+
return value as DBRecord;
|
|
37
|
+
}
|
|
38
|
+
|
|
26
39
|
export default class DB {
|
|
40
|
+
static instance: DB;
|
|
41
|
+
record!: DBRecord;
|
|
42
|
+
|
|
27
43
|
constructor() {
|
|
28
44
|
if (DB.instance) return DB.instance;
|
|
29
45
|
|
|
30
46
|
DB.instance = this;
|
|
31
47
|
}
|
|
32
48
|
|
|
33
|
-
async getSchema() {
|
|
49
|
+
async getSchema(): Promise<unknown> {
|
|
34
50
|
const { rootPath } = config;
|
|
35
51
|
const { file, schema } = config.orm.db;
|
|
36
52
|
|
|
@@ -39,10 +55,10 @@ export default class DB {
|
|
|
39
55
|
return (await import(`${rootPath}/${schema}`)).default;
|
|
40
56
|
}
|
|
41
57
|
|
|
42
|
-
getCollectionKeys() {
|
|
43
|
-
const SchemaClass = Orm.instance.models[`${dbKey}Model`]
|
|
58
|
+
getCollectionKeys(): string[] {
|
|
59
|
+
const SchemaClass = Orm.instance.models[`${dbKey}Model`] as new () => Record<string, unknown>;
|
|
44
60
|
const instance = new SchemaClass();
|
|
45
|
-
const keys = [];
|
|
61
|
+
const keys: string[] = [];
|
|
46
62
|
|
|
47
63
|
for (const key of Object.keys(instance)) {
|
|
48
64
|
if (key === '__name' || key === 'id') continue;
|
|
@@ -52,7 +68,7 @@ export default class DB {
|
|
|
52
68
|
return keys;
|
|
53
69
|
}
|
|
54
70
|
|
|
55
|
-
getDirPath() {
|
|
71
|
+
getDirPath(): string {
|
|
56
72
|
const { rootPath } = config;
|
|
57
73
|
const { file, directory } = config.orm.db;
|
|
58
74
|
const dbDir = path.dirname(path.resolve(`${rootPath}/${file}`));
|
|
@@ -60,7 +76,7 @@ export default class DB {
|
|
|
60
76
|
return path.join(dbDir, directory);
|
|
61
77
|
}
|
|
62
78
|
|
|
63
|
-
async validateMode() {
|
|
79
|
+
async validateMode(): Promise<void> {
|
|
64
80
|
const { rootPath } = config;
|
|
65
81
|
const { file, mode } = config.orm.db;
|
|
66
82
|
const collectionKeys = this.getCollectionKeys();
|
|
@@ -71,11 +87,11 @@ export default class DB {
|
|
|
71
87
|
const exists = await fileExists(dbFilePath);
|
|
72
88
|
|
|
73
89
|
if (exists) {
|
|
74
|
-
const data = await readFile(dbFilePath, { json: true })
|
|
90
|
+
const data = await readFile(dbFilePath, { json: true }) as Record<string, unknown[]>;
|
|
75
91
|
const hasData = collectionKeys.some(key => Array.isArray(data[key]) && data[key].length > 0);
|
|
76
92
|
|
|
77
93
|
if (hasData) {
|
|
78
|
-
log.error(`DB mode mismatch: db.json contains data but mode is set to 'directory'. Run migration first:\n\n stonyx db:migrate-to-directory\n`);
|
|
94
|
+
log.error?.(`DB mode mismatch: db.json contains data but mode is set to 'directory'. Run migration first:\n\n stonyx db:migrate-to-directory\n`);
|
|
79
95
|
process.exit(1);
|
|
80
96
|
}
|
|
81
97
|
}
|
|
@@ -88,14 +104,14 @@ export default class DB {
|
|
|
88
104
|
)).some(Boolean);
|
|
89
105
|
|
|
90
106
|
if (hasCollectionFiles) {
|
|
91
|
-
log.error(`DB mode mismatch: directory '${config.orm.db.directory}/' contains collection files but mode is set to 'file'. Run migration first:\n\n stonyx db:migrate-to-file\n`);
|
|
107
|
+
log.error?.(`DB mode mismatch: directory '${config.orm.db.directory}/' contains collection files but mode is set to 'file'. Run migration first:\n\n stonyx db:migrate-to-file\n`);
|
|
92
108
|
process.exit(1);
|
|
93
109
|
}
|
|
94
110
|
}
|
|
95
111
|
}
|
|
96
112
|
}
|
|
97
113
|
|
|
98
|
-
async init() {
|
|
114
|
+
async init(): Promise<void> {
|
|
99
115
|
const { autosave, saveInterval } = config.orm.db;
|
|
100
116
|
|
|
101
117
|
store.set(dbKey, new Map());
|
|
@@ -109,7 +125,7 @@ export default class DB {
|
|
|
109
125
|
new Cron().register('save', this.save.bind(this), saveInterval);
|
|
110
126
|
}
|
|
111
127
|
|
|
112
|
-
async create() {
|
|
128
|
+
async create(): Promise<Record<string, unknown>> {
|
|
113
129
|
const { rootPath } = config;
|
|
114
130
|
const { file, mode } = config.orm.db;
|
|
115
131
|
|
|
@@ -124,7 +140,7 @@ export default class DB {
|
|
|
124
140
|
));
|
|
125
141
|
|
|
126
142
|
// Write empty-array skeleton to db.json
|
|
127
|
-
const skeleton = {};
|
|
143
|
+
const skeleton: Record<string, unknown[]> = {};
|
|
128
144
|
for (const key of collectionKeys) skeleton[key] = [];
|
|
129
145
|
|
|
130
146
|
await createFile(`${rootPath}/${file}`, skeleton, { json: true });
|
|
@@ -137,9 +153,9 @@ export default class DB {
|
|
|
137
153
|
return {};
|
|
138
154
|
}
|
|
139
155
|
|
|
140
|
-
async save() {
|
|
156
|
+
async save(): Promise<void> {
|
|
141
157
|
const { file, mode } = config.orm.db;
|
|
142
|
-
const jsonData = this.record.format()
|
|
158
|
+
const jsonData = this.record.format() as Record<string, unknown>;
|
|
143
159
|
delete jsonData.id; // Don't save id
|
|
144
160
|
|
|
145
161
|
if (mode === 'directory') {
|
|
@@ -147,26 +163,36 @@ export default class DB {
|
|
|
147
163
|
const collectionKeys = this.getCollectionKeys();
|
|
148
164
|
|
|
149
165
|
// Write each collection to its own file in parallel
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
166
|
+
// Use createFile for new files, updateFile for existing ones
|
|
167
|
+
await Promise.all(collectionKeys.map(async key => {
|
|
168
|
+
const filePath = path.join(dirPath, `${key}.json`);
|
|
169
|
+
const exists = await fileExists(filePath);
|
|
170
|
+
const data = jsonData[key] || [];
|
|
171
|
+
|
|
172
|
+
if (exists) await updateFile(filePath, data, { json: true });
|
|
173
|
+
else await createFile(filePath, data, { json: true });
|
|
174
|
+
}));
|
|
153
175
|
|
|
154
176
|
// Write empty-array skeleton to db.json
|
|
155
|
-
const skeleton = {};
|
|
177
|
+
const skeleton: Record<string, unknown[]> = {};
|
|
156
178
|
for (const key of collectionKeys) skeleton[key] = [];
|
|
157
179
|
|
|
158
|
-
|
|
180
|
+
const dbFilePath = `${config.rootPath}/${file}`;
|
|
181
|
+
const dbFileExists = await fileExists(dbFilePath);
|
|
182
|
+
|
|
183
|
+
if (dbFileExists) await updateFile(dbFilePath, skeleton, { json: true });
|
|
184
|
+
else await createFile(dbFilePath, skeleton, { json: true });
|
|
159
185
|
|
|
160
|
-
log.db(`DB has been successfully saved to ${config.orm.db.directory}/ directory`);
|
|
186
|
+
log.db?.(`DB has been successfully saved to ${config.orm.db.directory}/ directory`);
|
|
161
187
|
return;
|
|
162
188
|
}
|
|
163
189
|
|
|
164
190
|
await updateFile(`${config.rootPath}/${file}`, jsonData, { json: true });
|
|
165
191
|
|
|
166
|
-
log.db(`DB has been successfully saved to ${file}`);
|
|
192
|
+
log.db?.(`DB has been successfully saved to ${file}`);
|
|
167
193
|
}
|
|
168
194
|
|
|
169
|
-
async getRecord() {
|
|
195
|
+
async getRecord(): Promise<DBRecord> {
|
|
170
196
|
const { mode } = config.orm.db;
|
|
171
197
|
|
|
172
198
|
if (mode === 'directory') return this.getRecordFromDirectory();
|
|
@@ -174,25 +200,25 @@ export default class DB {
|
|
|
174
200
|
return this.getRecordFromFile();
|
|
175
201
|
}
|
|
176
202
|
|
|
177
|
-
async getRecordFromFile() {
|
|
203
|
+
async getRecordFromFile(): Promise<DBRecord> {
|
|
178
204
|
const { file } = config.orm.db;
|
|
179
205
|
|
|
180
206
|
const data = await readFile(file, { json: true, missingFileCallback: this.create.bind(this) });
|
|
181
207
|
|
|
182
|
-
return createRecord(dbKey, data, { isDbRecord: true, serialize: false, transform: false });
|
|
208
|
+
return asDBRecord(createRecord(dbKey, data as Record<string, unknown>, { isDbRecord: true, serialize: false, transform: false }));
|
|
183
209
|
}
|
|
184
210
|
|
|
185
|
-
async getRecordFromDirectory() {
|
|
211
|
+
async getRecordFromDirectory(): Promise<DBRecord> {
|
|
186
212
|
const dirPath = this.getDirPath();
|
|
187
213
|
const collectionKeys = this.getCollectionKeys();
|
|
188
214
|
const dirExists = await fileExists(dirPath);
|
|
189
215
|
|
|
190
216
|
if (!dirExists) {
|
|
191
217
|
const data = await this.create();
|
|
192
|
-
return createRecord(dbKey, data, { isDbRecord: true, serialize: false, transform: false });
|
|
218
|
+
return asDBRecord(createRecord(dbKey, data, { isDbRecord: true, serialize: false, transform: false }));
|
|
193
219
|
}
|
|
194
220
|
|
|
195
|
-
const assembled = {};
|
|
221
|
+
const assembled: Record<string, unknown> = {};
|
|
196
222
|
|
|
197
223
|
await Promise.all(collectionKeys.map(async key => {
|
|
198
224
|
const filePath = path.join(dirPath, `${key}.json`);
|
|
@@ -201,6 +227,6 @@ export default class DB {
|
|
|
201
227
|
assembled[key] = exists ? await readFile(filePath, { json: true }) : [];
|
|
202
228
|
}));
|
|
203
229
|
|
|
204
|
-
return createRecord(dbKey, assembled, { isDbRecord: true, serialize: false, transform: false });
|
|
230
|
+
return asDBRecord(createRecord(dbKey, assembled, { isDbRecord: true, serialize: false, transform: false }));
|
|
205
231
|
}
|
|
206
232
|
}
|
package/src/has-many.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createRecord, store } from '@stonyx/orm';
|
|
2
|
+
import { getRelationships, getGlobalRegistry, getPendingRegistry, getBelongsToRegistry } from './relationships.js';
|
|
3
|
+
import { getOrSet, makeArray } from '@stonyx/utils/object';
|
|
4
|
+
import { dbKey } from './db.js';
|
|
5
|
+
import type { SourceRecord } from './types/orm-types.js';
|
|
6
|
+
|
|
7
|
+
interface HasManyOptions {
|
|
8
|
+
global?: boolean;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface PendingItem {
|
|
13
|
+
pendingRelationship: Map<unknown, unknown[][]>;
|
|
14
|
+
id: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type RelationshipHandler = ((sourceRecord: SourceRecord, rawData: unknown, options: HasManyOptions) => unknown[]) & {
|
|
18
|
+
__relatedModelName: string;
|
|
19
|
+
__relationshipType: 'hasMany';
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function queuePendingRelationship(
|
|
23
|
+
pendingRelationshipQueue: PendingItem[],
|
|
24
|
+
pendingRelationships: Map<string, Map<unknown, unknown[][]>>,
|
|
25
|
+
modelName: string,
|
|
26
|
+
id: unknown
|
|
27
|
+
): null {
|
|
28
|
+
pendingRelationshipQueue.push({
|
|
29
|
+
pendingRelationship: getOrSet(pendingRelationships, modelName, new Map()),
|
|
30
|
+
id
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default function hasMany(modelName: string): RelationshipHandler {
|
|
37
|
+
const globalRelationships = getGlobalRegistry();
|
|
38
|
+
const pendingRelationships = getPendingRegistry();
|
|
39
|
+
|
|
40
|
+
const fn = (sourceRecord: SourceRecord, rawData: unknown, options: HasManyOptions): unknown[] => {
|
|
41
|
+
const { __name: sourceModelName } = sourceRecord.__model;
|
|
42
|
+
const relationshipId = sourceRecord.id;
|
|
43
|
+
const relationship = getRelationships('hasMany', sourceModelName, modelName, relationshipId as string) as Map<unknown, unknown[]>;
|
|
44
|
+
const modelStore = store.get(modelName);
|
|
45
|
+
const pendingRelationshipQueue: PendingItem[] = [];
|
|
46
|
+
|
|
47
|
+
const output: unknown[] = !rawData ? [] : makeArray(rawData).map((elementData: unknown) => {
|
|
48
|
+
let record: unknown;
|
|
49
|
+
|
|
50
|
+
if (typeof elementData !== 'object') {
|
|
51
|
+
if (!modelStore) {
|
|
52
|
+
return queuePendingRelationship(pendingRelationshipQueue, pendingRelationships, modelName, elementData);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
record = modelStore.get(elementData as number | string);
|
|
56
|
+
|
|
57
|
+
if (!record) {
|
|
58
|
+
return queuePendingRelationship(pendingRelationshipQueue, pendingRelationships, modelName, elementData);
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
if (elementData !== Object(elementData)) {
|
|
62
|
+
return queuePendingRelationship(pendingRelationshipQueue, pendingRelationships, modelName, elementData);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
record = createRecord(modelName, elementData as Record<string, unknown>, options);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Populate belongTo side if the relationship is defined
|
|
69
|
+
const recordWithId = typeof record === 'object' && record !== null && 'id' in record ? record as SourceRecord : undefined;
|
|
70
|
+
const otherSide = recordWithId ? getBelongsToRegistry()
|
|
71
|
+
.get(modelName)?.get(sourceModelName)?.get(recordWithId.id) : undefined;
|
|
72
|
+
|
|
73
|
+
if (otherSide) Object.assign(otherSide, sourceRecord);
|
|
74
|
+
|
|
75
|
+
return record;
|
|
76
|
+
}).filter((value: unknown) => value);
|
|
77
|
+
|
|
78
|
+
relationship.set(relationshipId, output);
|
|
79
|
+
|
|
80
|
+
// Assign global relationship
|
|
81
|
+
if (options.global || sourceModelName === dbKey) getOrSet(globalRelationships, modelName, []).push(output);
|
|
82
|
+
|
|
83
|
+
// Assign pending relationships
|
|
84
|
+
for (const { pendingRelationship, id } of pendingRelationshipQueue) getOrSet(pendingRelationship, id, []).push(output);
|
|
85
|
+
|
|
86
|
+
return output;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
Object.defineProperty(fn, '__relatedModelName', { value: modelName });
|
|
90
|
+
Object.defineProperty(fn, '__relationshipType', { value: 'hasMany' as const });
|
|
91
|
+
return fn as RelationshipHandler;
|
|
92
|
+
}
|
|
@@ -19,26 +19,29 @@
|
|
|
19
19
|
* Unlike event-based hooks, middleware hooks run sequentially and can halt operations.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
type HookHandler = (context: Record<string, unknown>) => unknown | Promise<unknown>;
|
|
23
|
+
|
|
22
24
|
// Map of "operation:model" -> handler[]
|
|
23
|
-
const beforeHooks = new Map();
|
|
24
|
-
const afterHooks = new Map();
|
|
25
|
+
const beforeHooks: Map<string, HookHandler[]> = new Map();
|
|
26
|
+
const afterHooks: Map<string, HookHandler[]> = new Map();
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* Register a before hook middleware that runs before the operation executes.
|
|
28
30
|
*
|
|
29
|
-
* @param
|
|
30
|
-
* @param
|
|
31
|
-
* @param
|
|
31
|
+
* @param operation - Operation name: 'create', 'update', 'delete', 'get', or 'list'
|
|
32
|
+
* @param model - Model name (e.g., 'user', 'animal')
|
|
33
|
+
* @param handler - Middleware function (context) => any
|
|
32
34
|
* - Return undefined to continue to next hook/handler
|
|
33
35
|
* - Return any value to halt operation (integer = HTTP status, object = response body)
|
|
34
|
-
* @returns
|
|
36
|
+
* @returns Unsubscribe function
|
|
35
37
|
*/
|
|
36
|
-
export function beforeHook(operation, model, handler) {
|
|
38
|
+
export function beforeHook(operation: string, model: string, handler: HookHandler): () => void {
|
|
37
39
|
const key = `${operation}:${model}`;
|
|
38
40
|
if (!beforeHooks.has(key)) {
|
|
39
41
|
beforeHooks.set(key, []);
|
|
40
42
|
}
|
|
41
|
-
beforeHooks.get(key)
|
|
43
|
+
const hooks = beforeHooks.get(key);
|
|
44
|
+
if (hooks) hooks.push(handler);
|
|
42
45
|
|
|
43
46
|
// Return unsubscribe function
|
|
44
47
|
return () => {
|
|
@@ -54,17 +57,18 @@ export function beforeHook(operation, model, handler) {
|
|
|
54
57
|
* Register an after hook middleware that runs after the operation completes.
|
|
55
58
|
* After hooks cannot halt operations (they run after completion).
|
|
56
59
|
*
|
|
57
|
-
* @param
|
|
58
|
-
* @param
|
|
59
|
-
* @param
|
|
60
|
-
* @returns
|
|
60
|
+
* @param operation - Operation name
|
|
61
|
+
* @param model - Model name
|
|
62
|
+
* @param handler - Middleware function (context) => void
|
|
63
|
+
* @returns Unsubscribe function
|
|
61
64
|
*/
|
|
62
|
-
export function afterHook(operation, model, handler) {
|
|
65
|
+
export function afterHook(operation: string, model: string, handler: HookHandler): () => void {
|
|
63
66
|
const key = `${operation}:${model}`;
|
|
64
67
|
if (!afterHooks.has(key)) {
|
|
65
68
|
afterHooks.set(key, []);
|
|
66
69
|
}
|
|
67
|
-
afterHooks.get(key)
|
|
70
|
+
const hooks = afterHooks.get(key);
|
|
71
|
+
if (hooks) hooks.push(handler);
|
|
68
72
|
|
|
69
73
|
// Return unsubscribe function
|
|
70
74
|
return () => {
|
|
@@ -78,22 +82,16 @@ export function afterHook(operation, model, handler) {
|
|
|
78
82
|
|
|
79
83
|
/**
|
|
80
84
|
* Get all before hooks for an operation:model combination.
|
|
81
|
-
* @param {string} operation
|
|
82
|
-
* @param {string} model
|
|
83
|
-
* @returns {Function[]}
|
|
84
85
|
*/
|
|
85
|
-
export function getBeforeHooks(operation, model) {
|
|
86
|
+
export function getBeforeHooks(operation: string, model: string): HookHandler[] {
|
|
86
87
|
const key = `${operation}:${model}`;
|
|
87
88
|
return beforeHooks.get(key) || [];
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
/**
|
|
91
92
|
* Get all after hooks for an operation:model combination.
|
|
92
|
-
* @param {string} operation
|
|
93
|
-
* @param {string} model
|
|
94
|
-
* @returns {Function[]}
|
|
95
93
|
*/
|
|
96
|
-
export function getAfterHooks(operation, model) {
|
|
94
|
+
export function getAfterHooks(operation: string, model: string): HookHandler[] {
|
|
97
95
|
const key = `${operation}:${model}`;
|
|
98
96
|
return afterHooks.get(key) || [];
|
|
99
97
|
}
|
|
@@ -101,11 +99,11 @@ export function getAfterHooks(operation, model) {
|
|
|
101
99
|
/**
|
|
102
100
|
* Clear registered hooks for a specific operation:model.
|
|
103
101
|
*
|
|
104
|
-
* @param
|
|
105
|
-
* @param
|
|
106
|
-
* @param
|
|
102
|
+
* @param operation - Operation name
|
|
103
|
+
* @param model - Model name
|
|
104
|
+
* @param type - 'before' or 'after' (if omitted, clears both)
|
|
107
105
|
*/
|
|
108
|
-
export function clearHook(operation, model, type) {
|
|
106
|
+
export function clearHook(operation: string, model: string, type?: 'before' | 'after'): void {
|
|
109
107
|
const key = `${operation}:${model}`;
|
|
110
108
|
if (!type || type === 'before') {
|
|
111
109
|
beforeHooks.set(key, []);
|
|
@@ -118,7 +116,7 @@ export function clearHook(operation, model, type) {
|
|
|
118
116
|
/**
|
|
119
117
|
* Clear all hooks (useful for testing).
|
|
120
118
|
*/
|
|
121
|
-
export function clearAllHooks() {
|
|
119
|
+
export function clearAllHooks(): void {
|
|
122
120
|
beforeHooks.clear();
|
|
123
121
|
afterHooks.clear();
|
|
124
122
|
}
|
|
@@ -15,21 +15,24 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import Model from './model.js';
|
|
18
|
+
import View from './view.js';
|
|
18
19
|
import Serializer from './serializer.js';
|
|
19
20
|
|
|
20
21
|
import attr from './attr.js';
|
|
21
22
|
import belongsTo from './belongs-to.js';
|
|
22
23
|
import hasMany from './has-many.js';
|
|
23
24
|
import { createRecord, updateRecord } from './manage-record.js';
|
|
25
|
+
import { count, avg, sum, min, max } from './aggregates.js';
|
|
24
26
|
|
|
25
27
|
export { default } from './main.js';
|
|
26
28
|
export { store, relationships } from './main.js';
|
|
27
|
-
export { Model, Serializer }; // base classes
|
|
29
|
+
export { Model, View, Serializer }; // base classes
|
|
28
30
|
export { attr, belongsTo, hasMany, createRecord, updateRecord }; // helpers
|
|
31
|
+
export { count, avg, sum, min, max }; // aggregate helpers
|
|
29
32
|
export { beforeHook, afterHook, clearHook, clearAllHooks } from './hooks.js'; // middleware hooks
|
|
30
33
|
|
|
31
34
|
// Store API:
|
|
32
|
-
// store.get(model, id)
|
|
33
|
-
// store.find(model, id)
|
|
34
|
-
// store.findAll(model)
|
|
35
|
-
// store.query(model, conditions)
|
|
35
|
+
// store.get(model, id) -- sync, memory-only
|
|
36
|
+
// store.find(model, id) -- async, MySQL for memory:false models
|
|
37
|
+
// store.findAll(model) -- async, all records
|
|
38
|
+
// store.query(model, conditions) -- async, always hits MySQL
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Stone Costa
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import DB from './db.js';
|
|
18
|
+
import config from 'stonyx/config';
|
|
19
|
+
import log from 'stonyx/log';
|
|
20
|
+
import { forEachFileImport } from '@stonyx/utils/file';
|
|
21
|
+
import { kebabCaseToPascalCase, pluralize } from '@stonyx/utils/string';
|
|
22
|
+
import { registerPluralName } from './plural-registry.js';
|
|
23
|
+
import setupRestServer from './setup-rest-server.js';
|
|
24
|
+
import baseTransforms from './transforms.js';
|
|
25
|
+
import Store from './store.js';
|
|
26
|
+
import Serializer from './serializer.js';
|
|
27
|
+
import { setup } from '@stonyx/events';
|
|
28
|
+
|
|
29
|
+
interface OrmOptions {
|
|
30
|
+
dbType?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SqlDb {
|
|
34
|
+
init(): Promise<unknown>;
|
|
35
|
+
startup(): Promise<void>;
|
|
36
|
+
shutdown(): Promise<void>;
|
|
37
|
+
persist(operation: string, model: string, context: unknown, response: unknown): Promise<void>;
|
|
38
|
+
findRecord(modelName: string, id: unknown): Promise<unknown>;
|
|
39
|
+
findAll(modelName: string, conditions?: Record<string, unknown>): Promise<unknown[]>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface OrmDB {
|
|
43
|
+
record: unknown;
|
|
44
|
+
save(): Promise<void>;
|
|
45
|
+
init(): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const defaultOptions: OrmOptions = {
|
|
49
|
+
dbType: 'json'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default class Orm {
|
|
53
|
+
static initialized: boolean = false;
|
|
54
|
+
static relationships: Map<string, Map<string, unknown>> = new Map();
|
|
55
|
+
static store: Store = new Store();
|
|
56
|
+
static instance: Orm;
|
|
57
|
+
static ready: unknown[];
|
|
58
|
+
|
|
59
|
+
models: Record<string, unknown> = {};
|
|
60
|
+
serializers: Record<string, unknown> = {};
|
|
61
|
+
views: Record<string, unknown> = {};
|
|
62
|
+
transforms: Record<string, (value: unknown) => unknown> = { ...baseTransforms };
|
|
63
|
+
warnings: Set<string> = new Set();
|
|
64
|
+
options!: OrmOptions;
|
|
65
|
+
sqlDb?: SqlDb;
|
|
66
|
+
db?: OrmDB | SqlDb;
|
|
67
|
+
|
|
68
|
+
constructor(options: OrmOptions = {}) {
|
|
69
|
+
if (Orm.instance) return Orm.instance;
|
|
70
|
+
|
|
71
|
+
const { relationships } = Orm;
|
|
72
|
+
|
|
73
|
+
// Declare relationship maps
|
|
74
|
+
for (const key of ['hasMany', 'belongsTo', 'global', 'pending', 'pendingBelongsTo']) {
|
|
75
|
+
relationships.set(key, new Map());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.options = { ...defaultOptions, ...options };
|
|
79
|
+
|
|
80
|
+
Orm.instance = this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async init(): Promise<void> {
|
|
84
|
+
const { paths, restServer } = config.orm;
|
|
85
|
+
|
|
86
|
+
const promises: Promise<unknown>[] = ['Model', 'Serializer', 'Transform'].map(type => {
|
|
87
|
+
const lowerCaseType = type.toLowerCase();
|
|
88
|
+
const path = paths[lowerCaseType];
|
|
89
|
+
|
|
90
|
+
if (!path) throw new Error(`Configuration Error: ORM path for "${type}" must be defined.`);
|
|
91
|
+
|
|
92
|
+
return forEachFileImport(path, (exported: unknown, { name }: { name: string }) => {
|
|
93
|
+
// Transforms keep their original name, everything else gets converted to PascalCase with the type suffix
|
|
94
|
+
const alias = type === 'Transform' ? name : `${kebabCaseToPascalCase(name)}${type}`;
|
|
95
|
+
|
|
96
|
+
if (type === 'Model') {
|
|
97
|
+
Orm.store.set(name, new Map());
|
|
98
|
+
registerPluralName(name, exported as { pluralName?: string });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const collection = this[pluralize(lowerCaseType) as keyof this] as Record<string, unknown>;
|
|
102
|
+
return collection[alias] = exported;
|
|
103
|
+
}, { ignoreAccessFailure: true, rawName: true, recursive: true, recursiveNaming: true });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Wait for imports before db & rest server setup
|
|
107
|
+
await Promise.all(promises);
|
|
108
|
+
|
|
109
|
+
// Discover views from paths.view (separate from model/serializer/transform)
|
|
110
|
+
if (paths.view) {
|
|
111
|
+
await forEachFileImport(paths.view, (exported: unknown, { name }: { name: string }) => {
|
|
112
|
+
const alias = `${kebabCaseToPascalCase(name)}View`;
|
|
113
|
+
Orm.store.set(name, new Map());
|
|
114
|
+
registerPluralName(name, exported as { pluralName?: string });
|
|
115
|
+
this.views[alias] = exported;
|
|
116
|
+
}, { ignoreAccessFailure: true, rawName: true, recursive: true, recursiveNaming: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Setup event names for hooks after models are loaded
|
|
120
|
+
const eventNames: string[] = [];
|
|
121
|
+
const operations = ['list', 'get', 'create', 'update', 'delete'];
|
|
122
|
+
const viewOperations = ['list', 'get'];
|
|
123
|
+
const timings = ['before', 'after'];
|
|
124
|
+
|
|
125
|
+
for (const modelName of Orm.store.data.keys()) {
|
|
126
|
+
const isView = this.isView(modelName);
|
|
127
|
+
const ops = isView ? viewOperations : operations;
|
|
128
|
+
|
|
129
|
+
for (const timing of timings) {
|
|
130
|
+
for (const operation of ops) {
|
|
131
|
+
eventNames.push(`${timing}:${operation}:${modelName}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
setup(eventNames);
|
|
137
|
+
|
|
138
|
+
if (config.orm.timescale) {
|
|
139
|
+
const { default: TimescaleDB } = await import('./timescale/timescale-db.js');
|
|
140
|
+
this.sqlDb = new TimescaleDB() as SqlDb;
|
|
141
|
+
this.db = this.sqlDb;
|
|
142
|
+
promises.push(this.sqlDb.init());
|
|
143
|
+
} else if (config.orm.postgres) {
|
|
144
|
+
const { default: PostgresDB } = await import('./postgres/postgres-db.js');
|
|
145
|
+
this.sqlDb = new PostgresDB() as SqlDb;
|
|
146
|
+
this.db = this.sqlDb;
|
|
147
|
+
promises.push(this.sqlDb.init());
|
|
148
|
+
} else if (config.orm.mysql) {
|
|
149
|
+
const { default: MysqlDB } = await import('./mysql/mysql-db.js');
|
|
150
|
+
this.sqlDb = new MysqlDB() as SqlDb;
|
|
151
|
+
this.db = this.sqlDb;
|
|
152
|
+
promises.push(this.sqlDb.init());
|
|
153
|
+
} else if (this.options.dbType !== 'none') {
|
|
154
|
+
const db = new DB();
|
|
155
|
+
this.db = db;
|
|
156
|
+
|
|
157
|
+
promises.push(db.init());
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (restServer.enabled === 'true') {
|
|
161
|
+
promises.push(setupRestServer(restServer.route, paths.access, restServer.metaRoute));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Wire up memory resolver so store.find() can check model memory flags
|
|
165
|
+
Orm.store._memoryResolver = (modelName: string): boolean => {
|
|
166
|
+
const { modelClass } = this.getRecordClasses(modelName);
|
|
167
|
+
return (modelClass as { memory?: boolean })?.memory === true;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Wire up SQL adapter reference for on-demand queries from store.find()/findAll()
|
|
171
|
+
if (this.sqlDb) {
|
|
172
|
+
Orm.store._sqlDb = this.sqlDb;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
Orm.ready = await Promise.all(promises);
|
|
176
|
+
Orm.initialized = true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async startup(): Promise<void> {
|
|
180
|
+
if (this.sqlDb) await this.sqlDb.startup();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async shutdown(): Promise<void> {
|
|
184
|
+
if (this.sqlDb) await this.sqlDb.shutdown();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
static get db(): OrmDB | SqlDb {
|
|
188
|
+
if (!Orm.initialized) throw new Error('ORM has not been initialized yet');
|
|
189
|
+
|
|
190
|
+
if (!Orm.instance.db) throw new Error('ORM database has not been initialized');
|
|
191
|
+
return Orm.instance.db;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
getRecordClasses(modelName: string): { modelClass: unknown; serializerClass: unknown } {
|
|
195
|
+
const modelClassPrefix = kebabCaseToPascalCase(modelName);
|
|
196
|
+
|
|
197
|
+
// Check views first, then models
|
|
198
|
+
const viewClass = this.views[`${modelClassPrefix}View`];
|
|
199
|
+
if (viewClass) {
|
|
200
|
+
return {
|
|
201
|
+
modelClass: viewClass,
|
|
202
|
+
serializerClass: this.serializers[`${modelClassPrefix}Serializer`] || Serializer
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
modelClass: this.models[`${modelClassPrefix}Model`],
|
|
208
|
+
serializerClass: this.serializers[`${modelClassPrefix}Serializer`] || Serializer
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
isView(modelName: string): boolean {
|
|
213
|
+
const modelClassPrefix = kebabCaseToPascalCase(modelName);
|
|
214
|
+
return !!this.views[`${modelClassPrefix}View`];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Queue warnings to avoid the same error from being logged in the same iteration
|
|
218
|
+
warn(message: string): void {
|
|
219
|
+
this.warnings.add(message);
|
|
220
|
+
|
|
221
|
+
setTimeout(() => {
|
|
222
|
+
this.warnings.forEach(warning => log.warn?.(warning));
|
|
223
|
+
this.warnings.clear();
|
|
224
|
+
}, 0);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export const store = Orm.store;
|
|
229
|
+
export const relationships = Orm.relationships;
|