@stonyx/orm 0.2.1-beta.9 → 0.2.1-beta.91
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 +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 +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 +28 -0
- package/dist/postgres/schema-introspector.js +280 -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 +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.ts +151 -0
- package/src/{index.js → index.ts} +11 -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.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 +310 -0
- package/src/mysql/{type-map.js → type-map.ts} +42 -37
- package/src/{orm-request.js → orm-request.ts} +187 -108
- 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 +343 -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.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 +49 -0
- package/src/types/orm-types.ts +146 -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/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/hooks.js +0 -124
- package/src/main.js +0 -148
- package/src/manage-record.js +0 -118
- package/src/model-property.js +0 -29
- package/src/model.js +0 -9
- package/src/mysql/migration-generator.js +0 -188
- package/src/mysql/mysql-db.js +0 -320
- package/src/mysql/schema-introspector.js +0 -158
- package/src/record.js +0 -127
- package/src/relationships.js +0 -43
- package/src/serializer.js +0 -138
- package/src/store.js +0 -211
- 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
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import Orm, { store } from '@stonyx/orm';
|
|
2
|
+
import { createRecord } from './manage-record.js';
|
|
3
|
+
import { AggregateProperty } from './aggregates.js';
|
|
4
|
+
import { get } from '@stonyx/utils/object';
|
|
5
|
+
import type { SourceRecord } from './types/orm-types.js';
|
|
6
|
+
|
|
7
|
+
interface ViewClass {
|
|
8
|
+
source?: string;
|
|
9
|
+
resolve?: Record<string, unknown>;
|
|
10
|
+
groupBy?: string;
|
|
11
|
+
new (viewName: string): Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default class ViewResolver {
|
|
15
|
+
viewName: string;
|
|
16
|
+
|
|
17
|
+
constructor(viewName: string) {
|
|
18
|
+
this.viewName = viewName;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async resolveAll(): Promise<unknown[]> {
|
|
22
|
+
const orm = Orm.instance;
|
|
23
|
+
const { modelClass: viewClass } = orm.getRecordClasses(this.viewName) as { modelClass: ViewClass | undefined; serializerClass: unknown };
|
|
24
|
+
|
|
25
|
+
if (!viewClass) return [];
|
|
26
|
+
|
|
27
|
+
const source = viewClass.source;
|
|
28
|
+
if (!source) return [];
|
|
29
|
+
|
|
30
|
+
const sourceRecords = await store.findAll(source) as SourceRecord[];
|
|
31
|
+
if (!sourceRecords || sourceRecords.length === 0) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const resolveMap = viewClass.resolve || {};
|
|
36
|
+
const viewInstance = new viewClass(this.viewName);
|
|
37
|
+
const aggregateFields: Record<string, AggregateProperty> = {};
|
|
38
|
+
const regularFields: Record<string, unknown> = {};
|
|
39
|
+
|
|
40
|
+
// Categorize fields on the view instance
|
|
41
|
+
for (const [key, value] of Object.entries(viewInstance)) {
|
|
42
|
+
if (key.startsWith('__')) continue;
|
|
43
|
+
if (key === 'id') continue;
|
|
44
|
+
|
|
45
|
+
if (value instanceof AggregateProperty) {
|
|
46
|
+
aggregateFields[key] = value;
|
|
47
|
+
} else if (typeof value !== 'function') {
|
|
48
|
+
// Regular attr or direct value — not a relationship handler
|
|
49
|
+
regularFields[key] = value;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const groupByField = viewClass.groupBy;
|
|
54
|
+
|
|
55
|
+
if (groupByField) {
|
|
56
|
+
return this._resolveGroupBy(sourceRecords, groupByField, aggregateFields, regularFields, resolveMap, viewClass);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return this._resolvePerRecord(sourceRecords, aggregateFields, regularFields, resolveMap, viewClass);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private _resolvePerRecord(
|
|
63
|
+
sourceRecords: SourceRecord[],
|
|
64
|
+
aggregateFields: Record<string, AggregateProperty>,
|
|
65
|
+
regularFields: Record<string, unknown>,
|
|
66
|
+
resolveMap: Record<string, unknown>,
|
|
67
|
+
viewClass: ViewClass
|
|
68
|
+
): unknown[] {
|
|
69
|
+
const results: unknown[] = [];
|
|
70
|
+
|
|
71
|
+
for (const sourceRecord of sourceRecords) {
|
|
72
|
+
const rawData: Record<string, unknown> = { id: sourceRecord.id };
|
|
73
|
+
|
|
74
|
+
// Compute aggregate fields from source record's relationships
|
|
75
|
+
for (const [key, aggProp] of Object.entries(aggregateFields)) {
|
|
76
|
+
if (!aggProp.relationship) continue;
|
|
77
|
+
const relatedRecords = sourceRecord.__relationships?.[aggProp.relationship]
|
|
78
|
+
|| sourceRecord[aggProp.relationship];
|
|
79
|
+
const relArray = Array.isArray(relatedRecords) ? relatedRecords : [];
|
|
80
|
+
rawData[key] = aggProp.compute(relArray);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Apply resolve map entries
|
|
84
|
+
for (const [key, resolver] of Object.entries(resolveMap)) {
|
|
85
|
+
if (typeof resolver === 'function') {
|
|
86
|
+
rawData[key] = resolver(sourceRecord);
|
|
87
|
+
} else if (typeof resolver === 'string') {
|
|
88
|
+
rawData[key] = get(sourceRecord.__data || sourceRecord, resolver)
|
|
89
|
+
?? get(sourceRecord, resolver);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Map regular attr fields from source record if not already set
|
|
94
|
+
for (const key of Object.keys(regularFields)) {
|
|
95
|
+
if (rawData[key] !== undefined) continue;
|
|
96
|
+
|
|
97
|
+
const sourceValue = sourceRecord.__data?.[key] ?? sourceRecord[key];
|
|
98
|
+
if (sourceValue !== undefined) {
|
|
99
|
+
rawData[key] = sourceValue;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Set belongsTo source relationship
|
|
104
|
+
const viewInstanceForRel = new viewClass(this.viewName);
|
|
105
|
+
for (const [key, value] of Object.entries(viewInstanceForRel)) {
|
|
106
|
+
if (typeof value === 'function' && key !== 'id') {
|
|
107
|
+
// This is a relationship handler — pass the source record id
|
|
108
|
+
rawData[key] = sourceRecord.id;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Clear existing record from store to allow re-resolution
|
|
113
|
+
const viewStore = store.get(this.viewName);
|
|
114
|
+
if (viewStore?.has(rawData.id as number | string)) {
|
|
115
|
+
viewStore.delete(rawData.id as number | string);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const record = createRecord(this.viewName, rawData, { isDbRecord: true });
|
|
119
|
+
results.push(record);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return results;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private _resolveGroupBy(
|
|
126
|
+
sourceRecords: SourceRecord[],
|
|
127
|
+
groupByField: string,
|
|
128
|
+
aggregateFields: Record<string, AggregateProperty>,
|
|
129
|
+
regularFields: Record<string, unknown>,
|
|
130
|
+
resolveMap: Record<string, unknown>,
|
|
131
|
+
viewClass: ViewClass
|
|
132
|
+
): unknown[] {
|
|
133
|
+
// Group source records by the groupBy field value
|
|
134
|
+
const groups = new Map<unknown, SourceRecord[]>();
|
|
135
|
+
for (const record of sourceRecords) {
|
|
136
|
+
const key = record.__data?.[groupByField] ?? record[groupByField];
|
|
137
|
+
if (!groups.has(key)) {
|
|
138
|
+
groups.set(key, []);
|
|
139
|
+
}
|
|
140
|
+
const group = groups.get(key);
|
|
141
|
+
if (group) group.push(record);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const results: unknown[] = [];
|
|
145
|
+
|
|
146
|
+
for (const [groupKey, groupRecords] of groups) {
|
|
147
|
+
const rawData: Record<string, unknown> = { id: groupKey };
|
|
148
|
+
|
|
149
|
+
// Compute aggregate fields
|
|
150
|
+
for (const [key, aggProp] of Object.entries(aggregateFields)) {
|
|
151
|
+
if (aggProp.relationship === undefined) {
|
|
152
|
+
// Field-level aggregate — compute over group records directly
|
|
153
|
+
rawData[key] = aggProp.compute(groupRecords);
|
|
154
|
+
} else {
|
|
155
|
+
// Relationship aggregate — flatten related records across all group members
|
|
156
|
+
if (!aggProp.relationship) continue;
|
|
157
|
+
const allRelated: unknown[] = [];
|
|
158
|
+
for (const record of groupRecords) {
|
|
159
|
+
const relatedRecords = record.__relationships?.[aggProp.relationship]
|
|
160
|
+
|| record[aggProp.relationship];
|
|
161
|
+
if (Array.isArray(relatedRecords)) {
|
|
162
|
+
allRelated.push(...relatedRecords);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
rawData[key] = aggProp.compute(allRelated as { __data?: Record<string, unknown>; [key: string]: unknown }[]);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Apply resolve map entries — functions receive the group array
|
|
170
|
+
for (const [key, resolver] of Object.entries(resolveMap)) {
|
|
171
|
+
if (typeof resolver === 'function') {
|
|
172
|
+
rawData[key] = resolver(groupRecords);
|
|
173
|
+
} else if (typeof resolver === 'string') {
|
|
174
|
+
// String path — take value from first record in group
|
|
175
|
+
const first = groupRecords[0];
|
|
176
|
+
rawData[key] = get(first.__data || first, resolver)
|
|
177
|
+
?? get(first, resolver);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Map regular attr fields from first record if not already set
|
|
182
|
+
for (const key of Object.keys(regularFields)) {
|
|
183
|
+
if (rawData[key] !== undefined) continue;
|
|
184
|
+
const first = groupRecords[0];
|
|
185
|
+
const sourceValue = first.__data?.[key] ?? first[key];
|
|
186
|
+
if (sourceValue !== undefined) {
|
|
187
|
+
rawData[key] = sourceValue;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Clear existing record from store to allow re-resolution
|
|
192
|
+
const viewStore = store.get(this.viewName);
|
|
193
|
+
if (viewStore?.has(rawData.id as number | string)) {
|
|
194
|
+
viewStore.delete(rawData.id as number | string);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const record = createRecord(this.viewName, rawData, { isDbRecord: true });
|
|
198
|
+
results.push(record);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return results;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async resolveOne(id: unknown): Promise<unknown> {
|
|
205
|
+
const all = await this.resolveAll();
|
|
206
|
+
return all.find((record: unknown) => {
|
|
207
|
+
const r = record as SourceRecord;
|
|
208
|
+
return r.id === id || r.id == id;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
package/src/view.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import attr from './attr.js';
|
|
2
|
+
|
|
3
|
+
export default class View {
|
|
4
|
+
static memory: boolean = false;
|
|
5
|
+
static readOnly: boolean = true;
|
|
6
|
+
static pluralName: string | undefined = undefined;
|
|
7
|
+
static source: string | undefined = undefined;
|
|
8
|
+
static groupBy: string | undefined = undefined;
|
|
9
|
+
static resolve: ((record: unknown) => unknown) | undefined = undefined;
|
|
10
|
+
|
|
11
|
+
id = attr('number');
|
|
12
|
+
__name: string;
|
|
13
|
+
|
|
14
|
+
constructor(name: string) {
|
|
15
|
+
this.__name = name;
|
|
16
|
+
|
|
17
|
+
// Enforce readOnly — cannot be overridden to false
|
|
18
|
+
if ((this.constructor as typeof View).readOnly !== true) {
|
|
19
|
+
throw new Error(`View '${name}' cannot override readOnly to false`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# Stonyx Code Style Rules
|
|
2
|
-
|
|
3
|
-
Strict prettier/eslint rules to apply across all Stonyx projects. These will be formalized into an ESLint/Prettier config once enough patterns are collected.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Rules
|
|
8
|
-
|
|
9
|
-
### 1. Destructure config objects in function signatures
|
|
10
|
-
|
|
11
|
-
When a function receives a config/options object and only uses specific properties, destructure them in the function signature rather than accessing them via dot notation in the body.
|
|
12
|
-
|
|
13
|
-
**Bad:**
|
|
14
|
-
```javascript
|
|
15
|
-
export async function getPool(mysqlConfig) {
|
|
16
|
-
pool = mysql.createPool({
|
|
17
|
-
host: mysqlConfig.host,
|
|
18
|
-
port: mysqlConfig.port,
|
|
19
|
-
user: mysqlConfig.user,
|
|
20
|
-
password: mysqlConfig.password,
|
|
21
|
-
database: mysqlConfig.database,
|
|
22
|
-
connectionLimit: mysqlConfig.connectionLimit,
|
|
23
|
-
// ...
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
**Good:**
|
|
29
|
-
```javascript
|
|
30
|
-
export async function getPool({ host, port, user, password, database, connectionLimit }) {
|
|
31
|
-
pool = mysql.createPool({
|
|
32
|
-
host,
|
|
33
|
-
port,
|
|
34
|
-
user,
|
|
35
|
-
password,
|
|
36
|
-
database,
|
|
37
|
-
connectionLimit,
|
|
38
|
-
// ...
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
**Source:** PR #14, `src/mysql/connection.js`
|
|
44
|
-
**ESLint rule (candidate):** `prefer-destructuring` (with custom config for function parameters)
|
package/.claude/hooks.md
DELETED
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
# Middleware Hooks System
|
|
2
|
-
|
|
3
|
-
The ORM provides a powerful middleware-based hook system that allows custom logic before and after CRUD operations. **Before hooks can halt operations** by returning a value.
|
|
4
|
-
|
|
5
|
-
## Architecture
|
|
6
|
-
|
|
7
|
-
**Hook Registry**: [src/hooks.js](src/hooks.js) - Stores before/after hooks in Maps
|
|
8
|
-
**Integration**: [src/orm-request.js](src/orm-request.js) - `_withHooks()` wrapper executes hooks
|
|
9
|
-
**Exports**: [src/index.js](src/index.js) - Exports `beforeHook`, `afterHook`, `clearHook`, `clearAllHooks`
|
|
10
|
-
|
|
11
|
-
## API
|
|
12
|
-
|
|
13
|
-
### `beforeHook(operation, model, handler)`
|
|
14
|
-
|
|
15
|
-
Register a before hook that runs before the operation executes.
|
|
16
|
-
|
|
17
|
-
```javascript
|
|
18
|
-
import { beforeHook } from '@stonyx/orm';
|
|
19
|
-
|
|
20
|
-
beforeHook('create', 'animal', (context) => {
|
|
21
|
-
// Validate, transform, authorize...
|
|
22
|
-
if (invalid) {
|
|
23
|
-
return 400; // Halt with status code
|
|
24
|
-
}
|
|
25
|
-
// Return undefined to continue
|
|
26
|
-
});
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
**Handler return values:**
|
|
30
|
-
- `undefined` / no return - Operation continues
|
|
31
|
-
- **Any other value** - Halts operation and returns that value:
|
|
32
|
-
- Integer (e.g., `403`) - HTTP status code
|
|
33
|
-
- Object - JSON response body
|
|
34
|
-
|
|
35
|
-
**Returns:** Unregister function
|
|
36
|
-
|
|
37
|
-
### `afterHook(operation, model, handler)`
|
|
38
|
-
|
|
39
|
-
Register an after hook that runs after the operation completes.
|
|
40
|
-
|
|
41
|
-
```javascript
|
|
42
|
-
import { afterHook } from '@stonyx/orm';
|
|
43
|
-
|
|
44
|
-
afterHook('update', 'animal', (context) => {
|
|
45
|
-
console.log(`Updated animal ${context.record.id}`);
|
|
46
|
-
// After hooks cannot halt (operation already complete)
|
|
47
|
-
});
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
**Returns:** Unregister function
|
|
51
|
-
|
|
52
|
-
### `clearHook(operation, model, [type])`
|
|
53
|
-
|
|
54
|
-
Clear registered hooks for a specific operation:model.
|
|
55
|
-
|
|
56
|
-
```javascript
|
|
57
|
-
import { clearHook } from '@stonyx/orm';
|
|
58
|
-
|
|
59
|
-
clearHook('create', 'animal'); // Clear both before and after
|
|
60
|
-
clearHook('create', 'animal', 'before'); // Clear only before hooks
|
|
61
|
-
clearHook('create', 'animal', 'after'); // Clear only after hooks
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
### `clearAllHooks()`
|
|
65
|
-
|
|
66
|
-
Clear all registered hooks (useful for testing).
|
|
67
|
-
|
|
68
|
-
```javascript
|
|
69
|
-
import { clearAllHooks } from '@stonyx/orm';
|
|
70
|
-
|
|
71
|
-
afterEach(() => {
|
|
72
|
-
clearAllHooks();
|
|
73
|
-
});
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## Operations
|
|
77
|
-
|
|
78
|
-
- `list` - GET collection (`/animals`)
|
|
79
|
-
- `get` - GET single record (`/animals/1`)
|
|
80
|
-
- `create` - POST new record (`/animals`)
|
|
81
|
-
- `update` - PATCH existing record (`/animals/1`)
|
|
82
|
-
- `delete` - DELETE record (`/animals/1`)
|
|
83
|
-
|
|
84
|
-
## Context Object
|
|
85
|
-
|
|
86
|
-
Each hook receives a context object:
|
|
87
|
-
|
|
88
|
-
```javascript
|
|
89
|
-
{
|
|
90
|
-
model: 'animal', // Model name
|
|
91
|
-
operation: 'create', // Operation type
|
|
92
|
-
request, // Express request object
|
|
93
|
-
params, // URL params (e.g., { id: 5 })
|
|
94
|
-
body, // Request body (POST/PATCH)
|
|
95
|
-
query, // Query parameters
|
|
96
|
-
state, // Request state (includes filter for access control)
|
|
97
|
-
|
|
98
|
-
// For update/delete operations:
|
|
99
|
-
oldState, // Deep copy of record BEFORE operation
|
|
100
|
-
|
|
101
|
-
// For after hooks only:
|
|
102
|
-
response, // Handler response
|
|
103
|
-
record, // Affected record (create/update/get)
|
|
104
|
-
records, // All records (list)
|
|
105
|
-
recordId, // Record ID (delete only, since record no longer exists)
|
|
106
|
-
}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
**Notes:**
|
|
110
|
-
- `oldState` is captured via `JSON.parse(JSON.stringify())` before operation executes
|
|
111
|
-
- For delete operations, `recordId` is available since the record may no longer exist
|
|
112
|
-
- `oldState` enables precise field-level change detection
|
|
113
|
-
|
|
114
|
-
## Implementation Details
|
|
115
|
-
|
|
116
|
-
**Hook Wrapper** (`src/orm-request.js`):
|
|
117
|
-
|
|
118
|
-
```javascript
|
|
119
|
-
_withHooks(operation, handler) {
|
|
120
|
-
return async (request, state) => {
|
|
121
|
-
const context = { model, operation, request, params, body, query, state };
|
|
122
|
-
|
|
123
|
-
// Capture old state for update/delete
|
|
124
|
-
if (operation === 'update' || operation === 'delete') {
|
|
125
|
-
const existingRecord = store.get(this.model, getId(request.params));
|
|
126
|
-
if (existingRecord) {
|
|
127
|
-
context.oldState = JSON.parse(JSON.stringify(existingRecord.__data || existingRecord));
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Run before hooks sequentially (can halt by returning a value)
|
|
132
|
-
for (const hook of getBeforeHooks(operation, this.model)) {
|
|
133
|
-
const result = await hook(context);
|
|
134
|
-
if (result !== undefined) {
|
|
135
|
-
return result; // Halt - return status/response
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Execute main handler
|
|
140
|
-
const response = await handler(request, state);
|
|
141
|
-
|
|
142
|
-
// Enrich context for after hooks
|
|
143
|
-
context.response = response;
|
|
144
|
-
context.record = /* fetched from store */;
|
|
145
|
-
context.records = /* for list operations */;
|
|
146
|
-
context.recordId = /* for delete operations */;
|
|
147
|
-
|
|
148
|
-
// Run after hooks sequentially
|
|
149
|
-
for (const hook of getAfterHooks(operation, this.model)) {
|
|
150
|
-
await hook(context);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return response;
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
## Usage Examples
|
|
159
|
-
|
|
160
|
-
### Validation (Halting)
|
|
161
|
-
|
|
162
|
-
```javascript
|
|
163
|
-
beforeHook('create', 'animal', (context) => {
|
|
164
|
-
const { age } = context.body.data.attributes;
|
|
165
|
-
if (age < 0) {
|
|
166
|
-
return 400; // Halt with Bad Request
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
### Custom Error Response
|
|
172
|
-
|
|
173
|
-
```javascript
|
|
174
|
-
beforeHook('delete', 'animal', (context) => {
|
|
175
|
-
const animal = store.get('animal', context.params.id);
|
|
176
|
-
if (animal.protected) {
|
|
177
|
-
return { errors: [{ detail: 'Cannot delete protected animals' }] };
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
### Change Detection with oldState
|
|
183
|
-
|
|
184
|
-
```javascript
|
|
185
|
-
afterHook('update', 'animal', (context) => {
|
|
186
|
-
if (!context.oldState) return;
|
|
187
|
-
|
|
188
|
-
// Detect specific field changes
|
|
189
|
-
if (context.oldState.owner !== context.record.owner) {
|
|
190
|
-
console.log(`Owner changed from ${context.oldState.owner} to ${context.record.owner}`);
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
### Audit Logging
|
|
196
|
-
|
|
197
|
-
```javascript
|
|
198
|
-
afterHook('update', 'animal', async (context) => {
|
|
199
|
-
const changes = {};
|
|
200
|
-
if (context.oldState) {
|
|
201
|
-
for (const [key, newValue] of Object.entries(context.record.__data)) {
|
|
202
|
-
if (context.oldState[key] !== newValue) {
|
|
203
|
-
changes[key] = { from: context.oldState[key], to: newValue };
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
await auditLog.create({
|
|
209
|
-
operation: 'update',
|
|
210
|
-
model: context.model,
|
|
211
|
-
recordId: context.record.id,
|
|
212
|
-
changes // { age: { from: 2, to: 3 } }
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
### Delete Auditing
|
|
218
|
-
|
|
219
|
-
```javascript
|
|
220
|
-
afterHook('delete', 'animal', async (context) => {
|
|
221
|
-
await auditLog.create({
|
|
222
|
-
operation: 'delete',
|
|
223
|
-
model: context.model,
|
|
224
|
-
recordId: context.recordId,
|
|
225
|
-
deletedData: context.oldState // Full snapshot
|
|
226
|
-
});
|
|
227
|
-
});
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
## Key Differences from Event-Based System
|
|
231
|
-
|
|
232
|
-
| Feature | Event-Based (Old) | Middleware-Based (Current) |
|
|
233
|
-
|---------|-------------------|---------------------------|
|
|
234
|
-
| Execution | Parallel (fire-and-forget) | Sequential |
|
|
235
|
-
| Can halt operation | No | Yes (return any value) |
|
|
236
|
-
| Error handling | Isolated (logged) | Propagated (halts operation) |
|
|
237
|
-
| Middleware order | Not guaranteed | Registration order |
|
|
238
|
-
| Context modification | Not reliable | Reliable (sequential) |
|
|
239
|
-
| API | `subscribe('before:create:animal')` | `beforeHook('create', 'animal')` |
|
|
240
|
-
|
|
241
|
-
## Testing
|
|
242
|
-
|
|
243
|
-
**Location**: `test/integration/orm-test.js`
|
|
244
|
-
**Coverage**: Comprehensive hook tests including:
|
|
245
|
-
- Before/after hooks for all operations
|
|
246
|
-
- Halting with status codes
|
|
247
|
-
- Halting with custom response objects
|
|
248
|
-
- Sequential execution order
|
|
249
|
-
- Unsubscribe functionality
|
|
250
|
-
- clearHook functionality
|