@stonyx/orm 0.2.1-beta.9 → 0.2.1-beta.90

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.
Files changed (171) hide show
  1. package/README.md +64 -6
  2. package/config/environment.js +37 -1
  3. package/dist/aggregates.d.ts +21 -0
  4. package/dist/aggregates.js +93 -0
  5. package/dist/attr.d.ts +2 -0
  6. package/dist/attr.js +22 -0
  7. package/dist/belongs-to.d.ts +11 -0
  8. package/dist/belongs-to.js +59 -0
  9. package/dist/cli.d.ts +22 -0
  10. package/dist/cli.js +148 -0
  11. package/dist/commands.d.ts +7 -0
  12. package/dist/commands.js +146 -0
  13. package/dist/db.d.ts +21 -0
  14. package/dist/db.js +180 -0
  15. package/dist/exports/db.d.ts +7 -0
  16. package/{src → dist}/exports/db.js +2 -4
  17. package/dist/has-many.d.ts +11 -0
  18. package/dist/has-many.js +58 -0
  19. package/dist/hooks.d.ts +62 -0
  20. package/dist/hooks.js +110 -0
  21. package/dist/index.d.ts +14 -0
  22. package/dist/index.js +34 -0
  23. package/dist/main.d.ts +46 -0
  24. package/dist/main.js +181 -0
  25. package/dist/manage-record.d.ts +13 -0
  26. package/dist/manage-record.js +123 -0
  27. package/dist/meta-request.d.ts +6 -0
  28. package/dist/meta-request.js +52 -0
  29. package/dist/migrate.d.ts +2 -0
  30. package/dist/migrate.js +57 -0
  31. package/dist/model-property.d.ts +9 -0
  32. package/dist/model-property.js +29 -0
  33. package/dist/model.d.ts +15 -0
  34. package/dist/model.js +18 -0
  35. package/dist/mysql/connection.d.ts +14 -0
  36. package/dist/mysql/connection.js +24 -0
  37. package/dist/mysql/migration-generator.d.ts +45 -0
  38. package/dist/mysql/migration-generator.js +254 -0
  39. package/dist/mysql/migration-runner.d.ts +12 -0
  40. package/dist/mysql/migration-runner.js +88 -0
  41. package/dist/mysql/mysql-db.d.ts +100 -0
  42. package/dist/mysql/mysql-db.js +425 -0
  43. package/dist/mysql/query-builder.d.ts +10 -0
  44. package/dist/mysql/query-builder.js +44 -0
  45. package/dist/mysql/schema-introspector.d.ts +19 -0
  46. package/dist/mysql/schema-introspector.js +291 -0
  47. package/dist/mysql/type-map.d.ts +21 -0
  48. package/dist/mysql/type-map.js +36 -0
  49. package/dist/orm-request.d.ts +38 -0
  50. package/dist/orm-request.js +474 -0
  51. package/dist/plural-registry.d.ts +4 -0
  52. package/dist/plural-registry.js +9 -0
  53. package/dist/postgres/connection.d.ts +15 -0
  54. package/dist/postgres/connection.js +32 -0
  55. package/dist/postgres/migration-generator.d.ts +45 -0
  56. package/dist/postgres/migration-generator.js +261 -0
  57. package/dist/postgres/migration-runner.d.ts +10 -0
  58. package/dist/postgres/migration-runner.js +87 -0
  59. package/dist/postgres/postgres-db.d.ts +119 -0
  60. package/dist/postgres/postgres-db.js +477 -0
  61. package/dist/postgres/query-builder.d.ts +27 -0
  62. package/dist/postgres/query-builder.js +98 -0
  63. package/dist/postgres/schema-introspector.d.ts +29 -0
  64. package/dist/postgres/schema-introspector.js +314 -0
  65. package/dist/postgres/type-map.d.ts +23 -0
  66. package/dist/postgres/type-map.js +56 -0
  67. package/dist/record.d.ts +75 -0
  68. package/dist/record.js +129 -0
  69. package/dist/relationships.d.ts +10 -0
  70. package/dist/relationships.js +41 -0
  71. package/dist/serializer.d.ts +17 -0
  72. package/dist/serializer.js +136 -0
  73. package/dist/setup-rest-server.d.ts +1 -0
  74. package/dist/setup-rest-server.js +52 -0
  75. package/dist/standalone-db.d.ts +58 -0
  76. package/dist/standalone-db.js +142 -0
  77. package/dist/store.d.ts +62 -0
  78. package/dist/store.js +286 -0
  79. package/dist/timescale/query-builder.d.ts +43 -0
  80. package/dist/timescale/query-builder.js +115 -0
  81. package/dist/timescale/timescale-db.d.ts +45 -0
  82. package/dist/timescale/timescale-db.js +84 -0
  83. package/dist/transforms.d.ts +2 -0
  84. package/dist/transforms.js +17 -0
  85. package/dist/types/orm-types.d.ts +142 -0
  86. package/dist/types/orm-types.js +1 -0
  87. package/dist/utils.d.ts +7 -0
  88. package/dist/utils.js +17 -0
  89. package/dist/view-resolver.d.ts +8 -0
  90. package/dist/view-resolver.js +171 -0
  91. package/dist/view.d.ts +11 -0
  92. package/dist/view.js +18 -0
  93. package/package.json +57 -15
  94. package/src/aggregates.ts +109 -0
  95. package/src/{attr.js → attr.ts} +2 -2
  96. package/src/belongs-to.ts +90 -0
  97. package/src/cli.ts +183 -0
  98. package/src/{commands.js → commands.ts} +179 -170
  99. package/src/{db.js → db.ts} +55 -29
  100. package/src/exports/db.ts +7 -0
  101. package/src/has-many.ts +92 -0
  102. package/src/{hooks.js → hooks.ts} +41 -27
  103. package/src/{index.js → index.ts} +11 -2
  104. package/src/main.ts +229 -0
  105. package/src/manage-record.ts +161 -0
  106. package/src/{meta-request.js → meta-request.ts} +17 -14
  107. package/src/{migrate.js → migrate.ts} +9 -9
  108. package/src/model-property.ts +35 -0
  109. package/src/model.ts +21 -0
  110. package/src/mysql/{connection.js → connection.ts} +43 -28
  111. package/src/mysql/migration-generator.ts +337 -0
  112. package/src/mysql/{migration-runner.js → migration-runner.ts} +121 -110
  113. package/src/mysql/mysql-db.ts +543 -0
  114. package/src/mysql/{query-builder.js → query-builder.ts} +69 -64
  115. package/src/mysql/schema-introspector.ts +358 -0
  116. package/src/mysql/{type-map.js → type-map.ts} +42 -37
  117. package/src/{orm-request.js → orm-request.ts} +186 -108
  118. package/src/plural-registry.ts +12 -0
  119. package/src/postgres/connection.ts +48 -0
  120. package/src/postgres/migration-generator.ts +348 -0
  121. package/src/postgres/migration-runner.ts +115 -0
  122. package/src/postgres/postgres-db.ts +616 -0
  123. package/src/postgres/query-builder.ts +148 -0
  124. package/src/postgres/schema-introspector.ts +386 -0
  125. package/src/postgres/type-map.ts +61 -0
  126. package/src/record.ts +186 -0
  127. package/src/relationships.ts +54 -0
  128. package/src/serializer.ts +161 -0
  129. package/src/{setup-rest-server.js → setup-rest-server.ts} +18 -16
  130. package/src/standalone-db.ts +185 -0
  131. package/src/store.ts +373 -0
  132. package/src/timescale/query-builder.ts +174 -0
  133. package/src/timescale/timescale-db.ts +119 -0
  134. package/src/transforms.ts +20 -0
  135. package/src/types/mysql2.d.ts +49 -0
  136. package/src/types/orm-types.ts +146 -0
  137. package/src/types/pg.d.ts +32 -0
  138. package/src/types/stonyx-cron.d.ts +5 -0
  139. package/src/types/stonyx-events.d.ts +4 -0
  140. package/src/types/stonyx-rest-server.d.ts +16 -0
  141. package/src/types/stonyx-utils.d.ts +33 -0
  142. package/src/types/stonyx.d.ts +21 -0
  143. package/src/utils.ts +22 -0
  144. package/src/view-resolver.ts +211 -0
  145. package/src/view.ts +22 -0
  146. package/.claude/code-style-rules.md +0 -44
  147. package/.claude/hooks.md +0 -250
  148. package/.claude/index.md +0 -279
  149. package/.claude/usage-patterns.md +0 -217
  150. package/.github/workflows/ci.yml +0 -16
  151. package/.github/workflows/publish.yml +0 -51
  152. package/improvements.md +0 -139
  153. package/project-structure.md +0 -343
  154. package/src/belongs-to.js +0 -63
  155. package/src/has-many.js +0 -61
  156. package/src/main.js +0 -148
  157. package/src/manage-record.js +0 -118
  158. package/src/model-property.js +0 -29
  159. package/src/model.js +0 -9
  160. package/src/mysql/migration-generator.js +0 -188
  161. package/src/mysql/mysql-db.js +0 -320
  162. package/src/mysql/schema-introspector.js +0 -158
  163. package/src/record.js +0 -127
  164. package/src/relationships.js +0 -43
  165. package/src/serializer.js +0 -138
  166. package/src/store.js +0 -211
  167. package/src/transforms.js +0 -20
  168. package/src/utils.js +0 -12
  169. package/test-events-setup.js +0 -41
  170. package/test-hooks-manual.js +0 -54
  171. package/test-hooks-with-logging.js +0 -52
@@ -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, { createRecord, store } from '@stonyx/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
- await Promise.all(collectionKeys.map(key =>
151
- updateFile(path.join(dirPath, `${key}.json`), jsonData[key] || [], { json: true })
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] || []) as Record<string, unknown> | unknown[];
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
- await updateFile(`${config.rootPath}/${file}`, skeleton, { json: true });
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
  }
@@ -0,0 +1,7 @@
1
+ import Orm from '@stonyx/orm';
2
+
3
+ const db = Orm.db as { record: unknown; save(): Promise<void> };
4
+
5
+ export default db;
6
+ export const data = db.record;
7
+ export const saveDB = db.save.bind(db);
@@ -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,45 @@
19
19
  * Unlike event-based hooks, middleware hooks run sequentially and can halt operations.
20
20
  */
21
21
 
22
+ export interface HookContext {
23
+ model: string;
24
+ operation: string;
25
+ request?: unknown;
26
+ params?: Record<string, string>;
27
+ body?: Record<string, unknown>;
28
+ query?: Record<string, string>;
29
+ state?: Record<string, unknown>;
30
+ oldState?: unknown;
31
+ recordId?: string | number;
32
+ response?: unknown;
33
+ record?: unknown;
34
+ records?: unknown[];
35
+ [key: string]: unknown;
36
+ }
37
+
38
+ type HookHandler = (context: HookContext) => unknown | Promise<unknown>;
39
+
22
40
  // Map of "operation:model" -> handler[]
23
- const beforeHooks = new Map();
24
- const afterHooks = new Map();
41
+ const beforeHooks: Map<string, HookHandler[]> = new Map();
42
+ const afterHooks: Map<string, HookHandler[]> = new Map();
25
43
 
26
44
  /**
27
45
  * Register a before hook middleware that runs before the operation executes.
28
46
  *
29
- * @param {string} operation - Operation name: 'create', 'update', 'delete', 'get', or 'list'
30
- * @param {string} model - Model name (e.g., 'user', 'animal')
31
- * @param {Function} handler - Middleware function (context) => any
47
+ * @param operation - Operation name: 'create', 'update', 'delete', 'get', or 'list'
48
+ * @param model - Model name (e.g., 'user', 'animal')
49
+ * @param handler - Middleware function (context) => any
32
50
  * - Return undefined to continue to next hook/handler
33
51
  * - Return any value to halt operation (integer = HTTP status, object = response body)
34
- * @returns {Function} Unsubscribe function
52
+ * @returns Unsubscribe function
35
53
  */
36
- export function beforeHook(operation, model, handler) {
54
+ export function beforeHook(operation: string, model: string, handler: HookHandler): () => void {
37
55
  const key = `${operation}:${model}`;
38
56
  if (!beforeHooks.has(key)) {
39
57
  beforeHooks.set(key, []);
40
58
  }
41
- beforeHooks.get(key).push(handler);
59
+ const hooks = beforeHooks.get(key);
60
+ if (hooks) hooks.push(handler);
42
61
 
43
62
  // Return unsubscribe function
44
63
  return () => {
@@ -54,17 +73,18 @@ export function beforeHook(operation, model, handler) {
54
73
  * Register an after hook middleware that runs after the operation completes.
55
74
  * After hooks cannot halt operations (they run after completion).
56
75
  *
57
- * @param {string} operation - Operation name
58
- * @param {string} model - Model name
59
- * @param {Function} handler - Middleware function (context) => void
60
- * @returns {Function} Unsubscribe function
76
+ * @param operation - Operation name
77
+ * @param model - Model name
78
+ * @param handler - Middleware function (context) => void
79
+ * @returns Unsubscribe function
61
80
  */
62
- export function afterHook(operation, model, handler) {
81
+ export function afterHook(operation: string, model: string, handler: HookHandler): () => void {
63
82
  const key = `${operation}:${model}`;
64
83
  if (!afterHooks.has(key)) {
65
84
  afterHooks.set(key, []);
66
85
  }
67
- afterHooks.get(key).push(handler);
86
+ const hooks = afterHooks.get(key);
87
+ if (hooks) hooks.push(handler);
68
88
 
69
89
  // Return unsubscribe function
70
90
  return () => {
@@ -78,22 +98,16 @@ export function afterHook(operation, model, handler) {
78
98
 
79
99
  /**
80
100
  * Get all before hooks for an operation:model combination.
81
- * @param {string} operation
82
- * @param {string} model
83
- * @returns {Function[]}
84
101
  */
85
- export function getBeforeHooks(operation, model) {
102
+ export function getBeforeHooks(operation: string, model: string): HookHandler[] {
86
103
  const key = `${operation}:${model}`;
87
104
  return beforeHooks.get(key) || [];
88
105
  }
89
106
 
90
107
  /**
91
108
  * Get all after hooks for an operation:model combination.
92
- * @param {string} operation
93
- * @param {string} model
94
- * @returns {Function[]}
95
109
  */
96
- export function getAfterHooks(operation, model) {
110
+ export function getAfterHooks(operation: string, model: string): HookHandler[] {
97
111
  const key = `${operation}:${model}`;
98
112
  return afterHooks.get(key) || [];
99
113
  }
@@ -101,11 +115,11 @@ export function getAfterHooks(operation, model) {
101
115
  /**
102
116
  * Clear registered hooks for a specific operation:model.
103
117
  *
104
- * @param {string} operation - Operation name
105
- * @param {string} model - Model name
106
- * @param {string} [type] - 'before' or 'after' (if omitted, clears both)
118
+ * @param operation - Operation name
119
+ * @param model - Model name
120
+ * @param type - 'before' or 'after' (if omitted, clears both)
107
121
  */
108
- export function clearHook(operation, model, type) {
122
+ export function clearHook(operation: string, model: string, type?: 'before' | 'after'): void {
109
123
  const key = `${operation}:${model}`;
110
124
  if (!type || type === 'before') {
111
125
  beforeHooks.set(key, []);
@@ -118,7 +132,7 @@ export function clearHook(operation, model, type) {
118
132
  /**
119
133
  * Clear all hooks (useful for testing).
120
134
  */
121
- export function clearAllHooks() {
135
+ export function clearAllHooks(): void {
122
136
  beforeHooks.clear();
123
137
  afterHooks.clear();
124
138
  }
@@ -15,15 +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
29
- export { beforeHook, afterHook, clearHook, clearAllHooks } from './hooks.js'; // middleware hooks
31
+ export { count, avg, sum, min, max }; // aggregate helpers
32
+ export { beforeHook, afterHook, clearHook, clearAllHooks } from './hooks.js'; // middleware hooks
33
+
34
+ // Store API:
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