@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/dist/db.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
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
|
+
import Cron from '@stonyx/cron';
|
|
17
|
+
import config from 'stonyx/config';
|
|
18
|
+
import log from 'stonyx/log';
|
|
19
|
+
import Orm, { store } from '@stonyx/orm';
|
|
20
|
+
import { createRecord } from './manage-record.js';
|
|
21
|
+
import { createFile, createDirectory, updateFile, readFile, fileExists } from '@stonyx/utils/file';
|
|
22
|
+
import path from 'path';
|
|
23
|
+
export const dbKey = '__db';
|
|
24
|
+
function asDBRecord(value) {
|
|
25
|
+
if (typeof value !== 'object' || value === null || typeof value.format !== 'function') {
|
|
26
|
+
throw new Error('createRecord did not return a valid DBRecord');
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
export default class DB {
|
|
31
|
+
static instance;
|
|
32
|
+
record;
|
|
33
|
+
constructor() {
|
|
34
|
+
if (DB.instance)
|
|
35
|
+
return DB.instance;
|
|
36
|
+
DB.instance = this;
|
|
37
|
+
}
|
|
38
|
+
async getSchema() {
|
|
39
|
+
const { rootPath } = config;
|
|
40
|
+
const { file, schema } = config.orm.db;
|
|
41
|
+
if (!file)
|
|
42
|
+
throw new Error('Configuration Error: ORM DB file path must be defined.');
|
|
43
|
+
return (await import(`${rootPath}/${schema}`)).default;
|
|
44
|
+
}
|
|
45
|
+
getCollectionKeys() {
|
|
46
|
+
const SchemaClass = Orm.instance.models[`${dbKey}Model`];
|
|
47
|
+
const instance = new SchemaClass();
|
|
48
|
+
const keys = [];
|
|
49
|
+
for (const key of Object.keys(instance)) {
|
|
50
|
+
if (key === '__name' || key === 'id')
|
|
51
|
+
continue;
|
|
52
|
+
if (typeof instance[key] === 'function')
|
|
53
|
+
keys.push(key);
|
|
54
|
+
}
|
|
55
|
+
return keys;
|
|
56
|
+
}
|
|
57
|
+
getDirPath() {
|
|
58
|
+
const { rootPath } = config;
|
|
59
|
+
const { file, directory } = config.orm.db;
|
|
60
|
+
const dbDir = path.dirname(path.resolve(`${rootPath}/${file}`));
|
|
61
|
+
return path.join(dbDir, directory);
|
|
62
|
+
}
|
|
63
|
+
async validateMode() {
|
|
64
|
+
const { rootPath } = config;
|
|
65
|
+
const { file, mode } = config.orm.db;
|
|
66
|
+
const collectionKeys = this.getCollectionKeys();
|
|
67
|
+
const dirPath = this.getDirPath();
|
|
68
|
+
if (mode === 'directory') {
|
|
69
|
+
const dbFilePath = path.resolve(`${rootPath}/${file}`);
|
|
70
|
+
const exists = await fileExists(dbFilePath);
|
|
71
|
+
if (exists) {
|
|
72
|
+
const data = await readFile(dbFilePath, { json: true });
|
|
73
|
+
const hasData = collectionKeys.some(key => Array.isArray(data[key]) && data[key].length > 0);
|
|
74
|
+
if (hasData) {
|
|
75
|
+
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`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
const dirExists = await fileExists(dirPath);
|
|
82
|
+
if (dirExists) {
|
|
83
|
+
const hasCollectionFiles = (await Promise.all(collectionKeys.map(key => fileExists(path.join(dirPath, `${key}.json`))))).some(Boolean);
|
|
84
|
+
if (hasCollectionFiles) {
|
|
85
|
+
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`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async init() {
|
|
92
|
+
const { autosave, saveInterval } = config.orm.db;
|
|
93
|
+
store.set(dbKey, new Map());
|
|
94
|
+
Orm.instance.models[`${dbKey}Model`] = await this.getSchema();
|
|
95
|
+
await this.validateMode();
|
|
96
|
+
this.record = await this.getRecord();
|
|
97
|
+
if (autosave !== 'true')
|
|
98
|
+
return;
|
|
99
|
+
new Cron().register('save', this.save.bind(this), saveInterval);
|
|
100
|
+
}
|
|
101
|
+
async create() {
|
|
102
|
+
const { rootPath } = config;
|
|
103
|
+
const { file, mode } = config.orm.db;
|
|
104
|
+
if (mode === 'directory') {
|
|
105
|
+
const dirPath = this.getDirPath();
|
|
106
|
+
const collectionKeys = this.getCollectionKeys();
|
|
107
|
+
await createDirectory(dirPath);
|
|
108
|
+
await Promise.all(collectionKeys.map(key => createFile(path.join(dirPath, `${key}.json`), [], { json: true })));
|
|
109
|
+
// Write empty-array skeleton to db.json
|
|
110
|
+
const skeleton = {};
|
|
111
|
+
for (const key of collectionKeys)
|
|
112
|
+
skeleton[key] = [];
|
|
113
|
+
await createFile(`${rootPath}/${file}`, skeleton, { json: true });
|
|
114
|
+
return skeleton;
|
|
115
|
+
}
|
|
116
|
+
createFile(`${rootPath}/${file}`, {}, { json: true });
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
async save() {
|
|
120
|
+
const { file, mode } = config.orm.db;
|
|
121
|
+
const jsonData = this.record.format();
|
|
122
|
+
delete jsonData.id; // Don't save id
|
|
123
|
+
if (mode === 'directory') {
|
|
124
|
+
const dirPath = this.getDirPath();
|
|
125
|
+
const collectionKeys = this.getCollectionKeys();
|
|
126
|
+
// Write each collection to its own file in parallel
|
|
127
|
+
// Use createFile for new files, updateFile for existing ones
|
|
128
|
+
await Promise.all(collectionKeys.map(async (key) => {
|
|
129
|
+
const filePath = path.join(dirPath, `${key}.json`);
|
|
130
|
+
const exists = await fileExists(filePath);
|
|
131
|
+
const data = (jsonData[key] || []);
|
|
132
|
+
if (exists)
|
|
133
|
+
await updateFile(filePath, data, { json: true });
|
|
134
|
+
else
|
|
135
|
+
await createFile(filePath, data, { json: true });
|
|
136
|
+
}));
|
|
137
|
+
// Write empty-array skeleton to db.json
|
|
138
|
+
const skeleton = {};
|
|
139
|
+
for (const key of collectionKeys)
|
|
140
|
+
skeleton[key] = [];
|
|
141
|
+
const dbFilePath = `${config.rootPath}/${file}`;
|
|
142
|
+
const dbFileExists = await fileExists(dbFilePath);
|
|
143
|
+
if (dbFileExists)
|
|
144
|
+
await updateFile(dbFilePath, skeleton, { json: true });
|
|
145
|
+
else
|
|
146
|
+
await createFile(dbFilePath, skeleton, { json: true });
|
|
147
|
+
log.db?.(`DB has been successfully saved to ${config.orm.db.directory}/ directory`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
await updateFile(`${config.rootPath}/${file}`, jsonData, { json: true });
|
|
151
|
+
log.db?.(`DB has been successfully saved to ${file}`);
|
|
152
|
+
}
|
|
153
|
+
async getRecord() {
|
|
154
|
+
const { mode } = config.orm.db;
|
|
155
|
+
if (mode === 'directory')
|
|
156
|
+
return this.getRecordFromDirectory();
|
|
157
|
+
return this.getRecordFromFile();
|
|
158
|
+
}
|
|
159
|
+
async getRecordFromFile() {
|
|
160
|
+
const { file } = config.orm.db;
|
|
161
|
+
const data = await readFile(file, { json: true, missingFileCallback: this.create.bind(this) });
|
|
162
|
+
return asDBRecord(createRecord(dbKey, data, { isDbRecord: true, serialize: false, transform: false }));
|
|
163
|
+
}
|
|
164
|
+
async getRecordFromDirectory() {
|
|
165
|
+
const dirPath = this.getDirPath();
|
|
166
|
+
const collectionKeys = this.getCollectionKeys();
|
|
167
|
+
const dirExists = await fileExists(dirPath);
|
|
168
|
+
if (!dirExists) {
|
|
169
|
+
const data = await this.create();
|
|
170
|
+
return asDBRecord(createRecord(dbKey, data, { isDbRecord: true, serialize: false, transform: false }));
|
|
171
|
+
}
|
|
172
|
+
const assembled = {};
|
|
173
|
+
await Promise.all(collectionKeys.map(async (key) => {
|
|
174
|
+
const filePath = path.join(dirPath, `${key}.json`);
|
|
175
|
+
const exists = await fileExists(filePath);
|
|
176
|
+
assembled[key] = exists ? await readFile(filePath, { json: true }) : [];
|
|
177
|
+
}));
|
|
178
|
+
return asDBRecord(createRecord(dbKey, assembled, { isDbRecord: true, serialize: false, transform: false }));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SourceRecord } from './types/orm-types.js';
|
|
2
|
+
interface HasManyOptions {
|
|
3
|
+
global?: boolean;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
type RelationshipHandler = ((sourceRecord: SourceRecord, rawData: unknown, options: HasManyOptions) => unknown[]) & {
|
|
7
|
+
__relatedModelName: string;
|
|
8
|
+
__relationshipType: 'hasMany';
|
|
9
|
+
};
|
|
10
|
+
export default function hasMany(modelName: string): RelationshipHandler;
|
|
11
|
+
export {};
|
package/dist/has-many.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
function queuePendingRelationship(pendingRelationshipQueue, pendingRelationships, modelName, id) {
|
|
6
|
+
pendingRelationshipQueue.push({
|
|
7
|
+
pendingRelationship: getOrSet(pendingRelationships, modelName, new Map()),
|
|
8
|
+
id
|
|
9
|
+
});
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
export default function hasMany(modelName) {
|
|
13
|
+
const globalRelationships = getGlobalRegistry();
|
|
14
|
+
const pendingRelationships = getPendingRegistry();
|
|
15
|
+
const fn = (sourceRecord, rawData, options) => {
|
|
16
|
+
const { __name: sourceModelName } = sourceRecord.__model;
|
|
17
|
+
const relationshipId = sourceRecord.id;
|
|
18
|
+
const relationship = getRelationships('hasMany', sourceModelName, modelName, relationshipId);
|
|
19
|
+
const modelStore = store.get(modelName);
|
|
20
|
+
const pendingRelationshipQueue = [];
|
|
21
|
+
const output = !rawData ? [] : makeArray(rawData).map((elementData) => {
|
|
22
|
+
let record;
|
|
23
|
+
if (typeof elementData !== 'object') {
|
|
24
|
+
if (!modelStore) {
|
|
25
|
+
return queuePendingRelationship(pendingRelationshipQueue, pendingRelationships, modelName, elementData);
|
|
26
|
+
}
|
|
27
|
+
record = modelStore.get(elementData);
|
|
28
|
+
if (!record) {
|
|
29
|
+
return queuePendingRelationship(pendingRelationshipQueue, pendingRelationships, modelName, elementData);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
if (elementData !== Object(elementData)) {
|
|
34
|
+
return queuePendingRelationship(pendingRelationshipQueue, pendingRelationships, modelName, elementData);
|
|
35
|
+
}
|
|
36
|
+
record = createRecord(modelName, elementData, options);
|
|
37
|
+
}
|
|
38
|
+
// Populate belongTo side if the relationship is defined
|
|
39
|
+
const recordWithId = typeof record === 'object' && record !== null && 'id' in record ? record : undefined;
|
|
40
|
+
const otherSide = recordWithId ? getBelongsToRegistry()
|
|
41
|
+
.get(modelName)?.get(sourceModelName)?.get(recordWithId.id) : undefined;
|
|
42
|
+
if (otherSide)
|
|
43
|
+
Object.assign(otherSide, sourceRecord);
|
|
44
|
+
return record;
|
|
45
|
+
}).filter((value) => value);
|
|
46
|
+
relationship.set(relationshipId, output);
|
|
47
|
+
// Assign global relationship
|
|
48
|
+
if (options.global || sourceModelName === dbKey)
|
|
49
|
+
getOrSet(globalRelationships, modelName, []).push(output);
|
|
50
|
+
// Assign pending relationships
|
|
51
|
+
for (const { pendingRelationship, id } of pendingRelationshipQueue)
|
|
52
|
+
getOrSet(pendingRelationship, id, []).push(output);
|
|
53
|
+
return output;
|
|
54
|
+
};
|
|
55
|
+
Object.defineProperty(fn, '__relatedModelName', { value: modelName });
|
|
56
|
+
Object.defineProperty(fn, '__relationshipType', { value: 'hasMany' });
|
|
57
|
+
return fn;
|
|
58
|
+
}
|
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middleware-based hooks registry for ORM operations.
|
|
3
|
+
* Unlike event-based hooks, middleware hooks run sequentially and can halt operations.
|
|
4
|
+
*/
|
|
5
|
+
/** Context object passed to before/after hook handlers. */
|
|
6
|
+
export interface HookContext {
|
|
7
|
+
/** Model name (e.g. 'user', 'animal'). */
|
|
8
|
+
model: string;
|
|
9
|
+
/** Operation name: 'create', 'update', 'delete', 'get', or 'list'. */
|
|
10
|
+
operation: string;
|
|
11
|
+
/** The incoming HTTP request object. */
|
|
12
|
+
request?: unknown;
|
|
13
|
+
/** URL route parameters (e.g. { id: '42' }). */
|
|
14
|
+
params?: Record<string, string>;
|
|
15
|
+
/** Parsed request body for create/update operations. */
|
|
16
|
+
body?: Record<string, unknown>;
|
|
17
|
+
/** URL query string parameters. */
|
|
18
|
+
query?: Record<string, string>;
|
|
19
|
+
/** Mutable state bag shared across hooks within a single request. */
|
|
20
|
+
state?: Record<string, unknown>;
|
|
21
|
+
/** Previous record state (available in update hooks). */
|
|
22
|
+
oldState?: unknown;
|
|
23
|
+
/** Target record ID for single-record operations. */
|
|
24
|
+
recordId?: string | number;
|
|
25
|
+
/** Response data (available in after hooks). */
|
|
26
|
+
response?: unknown;
|
|
27
|
+
/** The affected record (available in after hooks for create/update/delete). */
|
|
28
|
+
record?: unknown;
|
|
29
|
+
/** The affected records (available in after hooks for list operations). */
|
|
30
|
+
records?: unknown[];
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
}
|
|
33
|
+
type HookHandler = (context: HookContext) => unknown | Promise<unknown>;
|
|
34
|
+
/**
|
|
35
|
+
* Register a before hook middleware that runs before the operation executes.
|
|
36
|
+
*
|
|
37
|
+
* @param operation - Operation name: 'create', 'update', 'delete', 'get', or 'list'
|
|
38
|
+
* @param model - Model name (e.g., 'user', 'animal')
|
|
39
|
+
* @param handler - Middleware function (context) => any
|
|
40
|
+
* - Return undefined to continue to next hook/handler
|
|
41
|
+
* - Return any value to halt operation (integer = HTTP status, object = response body)
|
|
42
|
+
* @returns Unsubscribe function
|
|
43
|
+
*/
|
|
44
|
+
export declare function beforeHook(operation: string, model: string, handler: HookHandler): () => void;
|
|
45
|
+
/**
|
|
46
|
+
* Register an after hook middleware that runs after the operation completes.
|
|
47
|
+
* After hooks cannot halt operations (they run after completion).
|
|
48
|
+
*
|
|
49
|
+
* @param operation - Operation name
|
|
50
|
+
* @param model - Model name
|
|
51
|
+
* @param handler - Middleware function (context) => void
|
|
52
|
+
* @returns Unsubscribe function
|
|
53
|
+
*/
|
|
54
|
+
export declare function afterHook(operation: string, model: string, handler: HookHandler): () => void;
|
|
55
|
+
/**
|
|
56
|
+
* Get all before hooks for an operation:model combination.
|
|
57
|
+
*/
|
|
58
|
+
export declare function getBeforeHooks(operation: string, model: string): HookHandler[];
|
|
59
|
+
/**
|
|
60
|
+
* Get all after hooks for an operation:model combination.
|
|
61
|
+
*/
|
|
62
|
+
export declare function getAfterHooks(operation: string, model: string): HookHandler[];
|
|
63
|
+
/**
|
|
64
|
+
* Clear registered hooks for a specific operation:model.
|
|
65
|
+
*
|
|
66
|
+
* @param operation - Operation name
|
|
67
|
+
* @param model - Model name
|
|
68
|
+
* @param type - 'before' or 'after' (if omitted, clears both)
|
|
69
|
+
*/
|
|
70
|
+
export declare function clearHook(operation: string, model: string, type?: 'before' | 'after'): void;
|
|
71
|
+
/**
|
|
72
|
+
* Clear all hooks (useful for testing).
|
|
73
|
+
*/
|
|
74
|
+
export declare function clearAllHooks(): void;
|
|
75
|
+
export {};
|
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
// Map of "operation:model" -> handler[]
|
|
17
|
+
const beforeHooks = new Map();
|
|
18
|
+
const afterHooks = new Map();
|
|
19
|
+
/**
|
|
20
|
+
* Register a before hook middleware that runs before the operation executes.
|
|
21
|
+
*
|
|
22
|
+
* @param operation - Operation name: 'create', 'update', 'delete', 'get', or 'list'
|
|
23
|
+
* @param model - Model name (e.g., 'user', 'animal')
|
|
24
|
+
* @param handler - Middleware function (context) => any
|
|
25
|
+
* - Return undefined to continue to next hook/handler
|
|
26
|
+
* - Return any value to halt operation (integer = HTTP status, object = response body)
|
|
27
|
+
* @returns Unsubscribe function
|
|
28
|
+
*/
|
|
29
|
+
export function beforeHook(operation, model, handler) {
|
|
30
|
+
const key = `${operation}:${model}`;
|
|
31
|
+
if (!beforeHooks.has(key)) {
|
|
32
|
+
beforeHooks.set(key, []);
|
|
33
|
+
}
|
|
34
|
+
const hooks = beforeHooks.get(key);
|
|
35
|
+
if (hooks)
|
|
36
|
+
hooks.push(handler);
|
|
37
|
+
// Return unsubscribe function
|
|
38
|
+
return () => {
|
|
39
|
+
const hooks = beforeHooks.get(key);
|
|
40
|
+
if (hooks) {
|
|
41
|
+
const index = hooks.indexOf(handler);
|
|
42
|
+
if (index > -1)
|
|
43
|
+
hooks.splice(index, 1);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Register an after hook middleware that runs after the operation completes.
|
|
49
|
+
* After hooks cannot halt operations (they run after completion).
|
|
50
|
+
*
|
|
51
|
+
* @param operation - Operation name
|
|
52
|
+
* @param model - Model name
|
|
53
|
+
* @param handler - Middleware function (context) => void
|
|
54
|
+
* @returns Unsubscribe function
|
|
55
|
+
*/
|
|
56
|
+
export function afterHook(operation, model, handler) {
|
|
57
|
+
const key = `${operation}:${model}`;
|
|
58
|
+
if (!afterHooks.has(key)) {
|
|
59
|
+
afterHooks.set(key, []);
|
|
60
|
+
}
|
|
61
|
+
const hooks = afterHooks.get(key);
|
|
62
|
+
if (hooks)
|
|
63
|
+
hooks.push(handler);
|
|
64
|
+
// Return unsubscribe function
|
|
65
|
+
return () => {
|
|
66
|
+
const hooks = afterHooks.get(key);
|
|
67
|
+
if (hooks) {
|
|
68
|
+
const index = hooks.indexOf(handler);
|
|
69
|
+
if (index > -1)
|
|
70
|
+
hooks.splice(index, 1);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get all before hooks for an operation:model combination.
|
|
76
|
+
*/
|
|
77
|
+
export function getBeforeHooks(operation, model) {
|
|
78
|
+
const key = `${operation}:${model}`;
|
|
79
|
+
return beforeHooks.get(key) || [];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get all after hooks for an operation:model combination.
|
|
83
|
+
*/
|
|
84
|
+
export function getAfterHooks(operation, model) {
|
|
85
|
+
const key = `${operation}:${model}`;
|
|
86
|
+
return afterHooks.get(key) || [];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Clear registered hooks for a specific operation:model.
|
|
90
|
+
*
|
|
91
|
+
* @param operation - Operation name
|
|
92
|
+
* @param model - Model name
|
|
93
|
+
* @param type - 'before' or 'after' (if omitted, clears both)
|
|
94
|
+
*/
|
|
95
|
+
export function clearHook(operation, model, type) {
|
|
96
|
+
const key = `${operation}:${model}`;
|
|
97
|
+
if (!type || type === 'before') {
|
|
98
|
+
beforeHooks.set(key, []);
|
|
99
|
+
}
|
|
100
|
+
if (!type || type === 'after') {
|
|
101
|
+
afterHooks.set(key, []);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Clear all hooks (useful for testing).
|
|
106
|
+
*/
|
|
107
|
+
export function clearAllHooks() {
|
|
108
|
+
beforeHooks.clear();
|
|
109
|
+
afterHooks.clear();
|
|
110
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import Model from './model.js';
|
|
2
|
+
import View from './view.js';
|
|
3
|
+
import Serializer from './serializer.js';
|
|
4
|
+
import attr from './attr.js';
|
|
5
|
+
import belongsTo from './belongs-to.js';
|
|
6
|
+
import hasMany from './has-many.js';
|
|
7
|
+
import { createRecord, updateRecord } from './manage-record.js';
|
|
8
|
+
import { count, avg, sum, min, max } from './aggregates.js';
|
|
9
|
+
export { default } from './main.js';
|
|
10
|
+
export { store, relationships } from './main.js';
|
|
11
|
+
export { Model, View, Serializer };
|
|
12
|
+
export { attr, belongsTo, hasMany, createRecord, updateRecord };
|
|
13
|
+
export { count, avg, sum, min, max };
|
|
14
|
+
export { beforeHook, afterHook, clearHook, clearAllHooks } from './hooks.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
import Model from './model.js';
|
|
17
|
+
import View from './view.js';
|
|
18
|
+
import Serializer from './serializer.js';
|
|
19
|
+
import attr from './attr.js';
|
|
20
|
+
import belongsTo from './belongs-to.js';
|
|
21
|
+
import hasMany from './has-many.js';
|
|
22
|
+
import { createRecord, updateRecord } from './manage-record.js';
|
|
23
|
+
import { count, avg, sum, min, max } from './aggregates.js';
|
|
24
|
+
export { default } from './main.js';
|
|
25
|
+
export { store, relationships } from './main.js';
|
|
26
|
+
export { Model, View, Serializer }; // base classes
|
|
27
|
+
export { attr, belongsTo, hasMany, createRecord, updateRecord }; // helpers
|
|
28
|
+
export { count, avg, sum, min, max }; // aggregate helpers
|
|
29
|
+
export { beforeHook, afterHook, clearHook, clearAllHooks } from './hooks.js'; // middleware hooks
|
|
30
|
+
// Store API:
|
|
31
|
+
// store.get(model, id) -- sync, memory-only
|
|
32
|
+
// store.find(model, id) -- async, MySQL for memory:false models
|
|
33
|
+
// store.findAll(model) -- async, all records
|
|
34
|
+
// store.query(model, conditions) -- async, always hits MySQL
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import Store from './store.js';
|
|
2
|
+
interface OrmOptions {
|
|
3
|
+
dbType?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface SqlDb {
|
|
6
|
+
init(): Promise<unknown>;
|
|
7
|
+
startup(): Promise<void>;
|
|
8
|
+
shutdown(): Promise<void>;
|
|
9
|
+
persist(operation: string, model: string, context: unknown, response: unknown): Promise<void>;
|
|
10
|
+
findRecord(modelName: string, id: unknown): Promise<unknown>;
|
|
11
|
+
findAll(modelName: string, conditions?: Record<string, unknown>): Promise<unknown[]>;
|
|
12
|
+
}
|
|
13
|
+
export interface OrmDB {
|
|
14
|
+
record: unknown;
|
|
15
|
+
save(): Promise<void>;
|
|
16
|
+
init(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export default class Orm {
|
|
19
|
+
static initialized: boolean;
|
|
20
|
+
static relationships: Map<string, Map<string, unknown>>;
|
|
21
|
+
static store: Store;
|
|
22
|
+
static instance: Orm;
|
|
23
|
+
static ready: unknown[];
|
|
24
|
+
models: Record<string, unknown>;
|
|
25
|
+
serializers: Record<string, unknown>;
|
|
26
|
+
views: Record<string, unknown>;
|
|
27
|
+
transforms: Record<string, (value: unknown) => unknown>;
|
|
28
|
+
warnings: Set<string>;
|
|
29
|
+
options: OrmOptions;
|
|
30
|
+
sqlDb?: SqlDb;
|
|
31
|
+
db?: OrmDB | SqlDb;
|
|
32
|
+
constructor(options?: OrmOptions);
|
|
33
|
+
init(): Promise<void>;
|
|
34
|
+
startup(): Promise<void>;
|
|
35
|
+
shutdown(): Promise<void>;
|
|
36
|
+
static get db(): OrmDB | SqlDb;
|
|
37
|
+
getRecordClasses(modelName: string): {
|
|
38
|
+
modelClass: unknown;
|
|
39
|
+
serializerClass: unknown;
|
|
40
|
+
};
|
|
41
|
+
isView(modelName: string): boolean;
|
|
42
|
+
warn(message: string): void;
|
|
43
|
+
}
|
|
44
|
+
export declare const store: Store;
|
|
45
|
+
export declare const relationships: Map<string, Map<string, unknown>>;
|
|
46
|
+
export {};
|