@stonyx/orm 0.3.2-beta.7 → 0.3.2-beta.8

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.
@@ -1,91 +1,12 @@
1
- const {
2
- ORM_ACCESS_PATH,
3
- ORM_MODEL_PATH,
4
- ORM_REST_ROUTE,
5
- ORM_SERIALIZER_PATH,
6
- ORM_TRANSFORM_PATH,
7
- ORM_VIEW_PATH,
8
- ORM_USE_REST_SERVER,
9
- DB_AUTO_SAVE,
10
- DB_FILE,
11
- DB_MODE,
12
- DB_DIRECTORY,
13
- DB_SCHEMA_PATH,
14
- DB_SAVE_INTERVAL,
15
- MYSQL_HOST,
16
- MYSQL_PORT,
17
- MYSQL_USER,
18
- MYSQL_PASSWORD,
19
- MYSQL_DATABASE,
20
- MYSQL_CONNECTION_LIMIT,
21
- MYSQL_MIGRATIONS_DIR,
22
- PG_HOST,
23
- PG_PORT,
24
- PG_USER,
25
- PG_PASSWORD,
26
- PG_DATABASE,
27
- PG_CONNECTION_LIMIT,
28
- PG_MIGRATIONS_DIR,
29
- TIMESCALE_HOST,
30
- TIMESCALE_PORT,
31
- TIMESCALE_USER,
32
- TIMESCALE_PASSWORD,
33
- TIMESCALE_DATABASE,
34
- TIMESCALE_CONNECTION_LIMIT,
35
- TIMESCALE_MIGRATIONS_DIR,
36
- } = process.env;
37
-
38
- export default {
39
- logColor: 'white',
40
- logMethod: 'db',
41
-
42
- db: {
43
- autosave: DB_AUTO_SAVE ?? 'false', // 'true' (cron interval), 'false' (disabled), 'onUpdate' (save after each write op)
44
- file: DB_FILE ?? 'db.json',
45
- mode: DB_MODE ?? 'file', // 'file' (single db.json) or 'directory' (one file per collection)
46
- directory: DB_DIRECTORY ?? 'db', // directory name for collection files when mode is 'directory'
47
- saveInterval: DB_SAVE_INTERVAL ?? 60 * 60, // 1 hour
48
- schema: DB_SCHEMA_PATH ?? './config/db-schema.js'
49
- },
50
- paths: {
51
- access: ORM_ACCESS_PATH ?? './access', // Optional for restServer access hooks
52
- model: ORM_MODEL_PATH ?? './models',
53
- serializer: ORM_SERIALIZER_PATH ?? './serializers',
54
- transform: ORM_TRANSFORM_PATH ?? './transforms',
55
- view: ORM_VIEW_PATH ?? './views'
56
- },
57
- mysql: MYSQL_HOST ? {
58
- host: MYSQL_HOST ?? 'localhost',
59
- port: parseInt(MYSQL_PORT ?? '3306'),
60
- user: MYSQL_USER ?? 'root',
61
- password: MYSQL_PASSWORD ?? '',
62
- database: MYSQL_DATABASE ?? 'stonyx',
63
- connectionLimit: parseInt(MYSQL_CONNECTION_LIMIT ?? '10'),
64
- migrationsDir: MYSQL_MIGRATIONS_DIR ?? 'migrations',
65
- migrationsTable: '__migrations',
66
- } : undefined,
67
- postgres: PG_HOST ? {
68
- host: PG_HOST ?? 'localhost',
69
- port: parseInt(PG_PORT ?? '5432'),
70
- user: PG_USER ?? 'postgres',
71
- password: PG_PASSWORD ?? '',
72
- database: PG_DATABASE ?? 'stonyx',
73
- connectionLimit: parseInt(PG_CONNECTION_LIMIT ?? '10'),
74
- migrationsDir: PG_MIGRATIONS_DIR ?? 'migrations',
75
- migrationsTable: '__migrations',
76
- } : undefined,
77
- timescale: TIMESCALE_HOST ? {
78
- host: TIMESCALE_HOST ?? 'localhost',
79
- port: parseInt(TIMESCALE_PORT ?? '5432'),
80
- user: TIMESCALE_USER ?? 'postgres',
81
- password: TIMESCALE_PASSWORD ?? '',
82
- database: TIMESCALE_DATABASE ?? 'stonyx',
83
- connectionLimit: parseInt(TIMESCALE_CONNECTION_LIMIT ?? '10'),
84
- migrationsDir: TIMESCALE_MIGRATIONS_DIR ?? 'migrations',
85
- migrationsTable: '__migrations',
86
- } : undefined,
87
- restServer: {
88
- enabled: ORM_USE_REST_SERVER ?? 'true', // Whether to load restServer for automatic route setup or
89
- route: ORM_REST_ROUTE ?? '/',
90
- }
91
- }
1
+ // project configuration, override-able by listed environment variables
2
+ const {
3
+ DEBUG,
4
+ NODE_ENV,
5
+ } = process.env;
6
+
7
+ const environment = NODE_ENV ?? 'development';
8
+
9
+ export default {
10
+ environment,
11
+ debug: DEBUG ?? environment === 'development',
12
+ }
@@ -0,0 +1,91 @@
1
+ const {
2
+ ORM_ACCESS_PATH,
3
+ ORM_MODEL_PATH,
4
+ ORM_REST_ROUTE,
5
+ ORM_SERIALIZER_PATH,
6
+ ORM_TRANSFORM_PATH,
7
+ ORM_VIEW_PATH,
8
+ ORM_USE_REST_SERVER,
9
+ DB_AUTO_SAVE,
10
+ DB_FILE,
11
+ DB_MODE,
12
+ DB_DIRECTORY,
13
+ DB_SCHEMA_PATH,
14
+ DB_SAVE_INTERVAL,
15
+ MYSQL_HOST,
16
+ MYSQL_PORT,
17
+ MYSQL_USER,
18
+ MYSQL_PASSWORD,
19
+ MYSQL_DATABASE,
20
+ MYSQL_CONNECTION_LIMIT,
21
+ MYSQL_MIGRATIONS_DIR,
22
+ PG_HOST,
23
+ PG_PORT,
24
+ PG_USER,
25
+ PG_PASSWORD,
26
+ PG_DATABASE,
27
+ PG_CONNECTION_LIMIT,
28
+ PG_MIGRATIONS_DIR,
29
+ TIMESCALE_HOST,
30
+ TIMESCALE_PORT,
31
+ TIMESCALE_USER,
32
+ TIMESCALE_PASSWORD,
33
+ TIMESCALE_DATABASE,
34
+ TIMESCALE_CONNECTION_LIMIT,
35
+ TIMESCALE_MIGRATIONS_DIR,
36
+ } = process.env;
37
+
38
+ export default {
39
+ logColor: 'white',
40
+ logMethod: 'db',
41
+
42
+ db: {
43
+ autosave: DB_AUTO_SAVE ?? 'false', // 'true' (cron interval), 'false' (disabled), 'onUpdate' (save after each write op)
44
+ file: DB_FILE ?? 'db.json',
45
+ mode: DB_MODE ?? 'file', // 'file' (single db.json) or 'directory' (one file per collection)
46
+ directory: DB_DIRECTORY ?? 'db', // directory name for collection files when mode is 'directory'
47
+ saveInterval: DB_SAVE_INTERVAL ?? 60 * 60, // 1 hour
48
+ schema: DB_SCHEMA_PATH ?? './config/db-schema.js'
49
+ },
50
+ paths: {
51
+ access: ORM_ACCESS_PATH ?? './access', // Optional for restServer access hooks
52
+ model: ORM_MODEL_PATH ?? './models',
53
+ serializer: ORM_SERIALIZER_PATH ?? './serializers',
54
+ transform: ORM_TRANSFORM_PATH ?? './transforms',
55
+ view: ORM_VIEW_PATH ?? './views'
56
+ },
57
+ mysql: MYSQL_HOST ? {
58
+ host: MYSQL_HOST ?? 'localhost',
59
+ port: parseInt(MYSQL_PORT ?? '3306'),
60
+ user: MYSQL_USER ?? 'root',
61
+ password: MYSQL_PASSWORD ?? '',
62
+ database: MYSQL_DATABASE ?? 'stonyx',
63
+ connectionLimit: parseInt(MYSQL_CONNECTION_LIMIT ?? '10'),
64
+ migrationsDir: MYSQL_MIGRATIONS_DIR ?? 'migrations',
65
+ migrationsTable: '__migrations',
66
+ } : undefined,
67
+ postgres: PG_HOST ? {
68
+ host: PG_HOST ?? 'localhost',
69
+ port: parseInt(PG_PORT ?? '5432'),
70
+ user: PG_USER ?? 'postgres',
71
+ password: PG_PASSWORD ?? '',
72
+ database: PG_DATABASE ?? 'stonyx',
73
+ connectionLimit: parseInt(PG_CONNECTION_LIMIT ?? '10'),
74
+ migrationsDir: PG_MIGRATIONS_DIR ?? 'migrations',
75
+ migrationsTable: '__migrations',
76
+ } : undefined,
77
+ timescale: TIMESCALE_HOST ? {
78
+ host: TIMESCALE_HOST ?? 'localhost',
79
+ port: parseInt(TIMESCALE_PORT ?? '5432'),
80
+ user: TIMESCALE_USER ?? 'postgres',
81
+ password: TIMESCALE_PASSWORD ?? '',
82
+ database: TIMESCALE_DATABASE ?? 'stonyx',
83
+ connectionLimit: parseInt(TIMESCALE_CONNECTION_LIMIT ?? '10'),
84
+ migrationsDir: TIMESCALE_MIGRATIONS_DIR ?? 'migrations',
85
+ migrationsTable: '__migrations',
86
+ } : undefined,
87
+ restServer: {
88
+ enabled: ORM_USE_REST_SERVER ?? 'true', // Whether to load restServer for automatic route setup or
89
+ route: ORM_REST_ROUTE ?? '/',
90
+ }
91
+ }
package/dist/index.d.ts CHANGED
@@ -8,6 +8,7 @@ import { createRecord, updateRecord } from './manage-record.js';
8
8
  import { count, avg, sum, min, max } from './aggregates.js';
9
9
  export { default } from './main.js';
10
10
  export { store, relationships } from './main.js';
11
+ export type { PersistErrorDetail } from './main.js';
11
12
  export { Model, View, Serializer };
12
13
  export { attr, belongsTo, hasMany, createRecord, updateRecord };
13
14
  export { count, avg, sum, min, max };
package/dist/main.d.ts CHANGED
@@ -15,6 +15,12 @@ export interface OrmDB {
15
15
  save(): Promise<void>;
16
16
  init(): Promise<void>;
17
17
  }
18
+ export interface PersistErrorDetail {
19
+ operation: 'create' | 'update' | 'delete';
20
+ modelName: string;
21
+ recordId: unknown;
22
+ error: Error;
23
+ }
18
24
  export default class Orm {
19
25
  static initialized: boolean;
20
26
  static relationships: Map<string, Map<string, unknown>>;
@@ -29,6 +35,7 @@ export default class Orm {
29
35
  options: OrmOptions;
30
36
  sqlDb?: SqlDb;
31
37
  db?: OrmDB | SqlDb;
38
+ private _persistErrorHandler;
32
39
  constructor(options?: OrmOptions);
33
40
  init(): Promise<void>;
34
41
  startup(): Promise<void>;
@@ -39,6 +46,15 @@ export default class Orm {
39
46
  serializerClass: unknown;
40
47
  };
41
48
  isView(modelName: string): boolean;
49
+ /**
50
+ * Register a callback to be invoked when a fire-and-forget SQL persist fails.
51
+ * Without a handler, persist errors are logged via log.error (backwards-compatible).
52
+ */
53
+ onPersistError(handler: ((detail: PersistErrorDetail) => void) | null): void;
54
+ /**
55
+ * Emit a persist error to the registered handler, or fall back to log.error.
56
+ */
57
+ emitPersistError(detail: PersistErrorDetail): void;
42
58
  warn(message: string): void;
43
59
  }
44
60
  export declare const store: Store;
package/dist/main.js CHANGED
@@ -41,6 +41,7 @@ export default class Orm {
41
41
  options;
42
42
  sqlDb;
43
43
  db;
44
+ _persistErrorHandler = null;
44
45
  constructor(options = {}) {
45
46
  if (Orm.instance)
46
47
  return Orm.instance;
@@ -168,6 +169,31 @@ export default class Orm {
168
169
  const modelClassPrefix = kebabCaseToPascalCase(modelName);
169
170
  return !!this.views[`${modelClassPrefix}View`];
170
171
  }
172
+ /**
173
+ * Register a callback to be invoked when a fire-and-forget SQL persist fails.
174
+ * Without a handler, persist errors are logged via log.error (backwards-compatible).
175
+ */
176
+ onPersistError(handler) {
177
+ this._persistErrorHandler = handler;
178
+ }
179
+ /**
180
+ * Emit a persist error to the registered handler, or fall back to log.error.
181
+ */
182
+ emitPersistError(detail) {
183
+ const fallbackLog = () => log.error?.(`[ORM] Failed to persist ${detail.operation} for ${detail.modelName}:${String(detail.recordId)}: ${detail.error.message}`);
184
+ if (this._persistErrorHandler) {
185
+ try {
186
+ this._persistErrorHandler(detail);
187
+ }
188
+ catch (handlerError) {
189
+ fallbackLog();
190
+ log.error?.(`[ORM] onPersistError handler threw: ${handlerError instanceof Error ? handlerError.message : String(handlerError)}`);
191
+ }
192
+ }
193
+ else {
194
+ fallbackLog();
195
+ }
196
+ }
171
197
  // Queue warnings to avoid the same error from being logged in the same iteration
172
198
  warn(message) {
173
199
  this.warnings.add(message);
@@ -2,7 +2,6 @@ import Orm, { store } from '@stonyx/orm';
2
2
  import OrmRecord from './record.js';
3
3
  import { getGlobalRegistry, getPendingRegistry, getPendingBelongsToRegistry, getBelongsToRegistry, getHasManyRegistry } from './relationships.js';
4
4
  import { isOrmRecord } from './utils.js';
5
- import log from 'stonyx/log';
6
5
  const defaultOptions = {
7
6
  isDbRecord: false,
8
7
  serialize: true,
@@ -86,7 +85,12 @@ export function createRecord(modelName, rawData = {}, userOptions = {}) {
86
85
  if (shouldPersist) {
87
86
  const response = { data: { id: record.id } };
88
87
  orm.sqlDb.persist('create', modelName, { rawData }, response).catch((err) => {
89
- log.error?.(`[ORM] Failed to persist create for ${modelName}:${String(record.id)}: ${err instanceof Error ? err.message : String(err)}`);
88
+ orm.emitPersistError({
89
+ operation: 'create',
90
+ modelName,
91
+ recordId: record.id,
92
+ error: err instanceof Error ? err : new Error(String(err)),
93
+ });
90
94
  });
91
95
  }
92
96
  return record;
@@ -108,7 +112,12 @@ export function updateRecord(record, rawData, userOptions = {}) {
108
112
  const shouldPersist = orm?.sqlDb && !options.isDbRecord && !userOptions._relationshipKey && !options._skipAutoPersist;
109
113
  if (shouldPersist && modelName) {
110
114
  orm.sqlDb.persist('update', modelName, { record, oldState }, {}).catch((err) => {
111
- log.error?.(`[ORM] Failed to persist update for ${modelName}:${String(record.id)}: ${err instanceof Error ? err.message : String(err)}`);
115
+ orm.emitPersistError({
116
+ operation: 'update',
117
+ modelName,
118
+ recordId: record.id,
119
+ error: err instanceof Error ? err : new Error(String(err)),
120
+ });
112
121
  });
113
122
  }
114
123
  }
package/dist/store.js CHANGED
@@ -115,7 +115,12 @@ export default class Store {
115
115
  // Auto-persist delete to SQL
116
116
  if (id && Orm.instance?.sqlDb) {
117
117
  Orm.instance.sqlDb.persist('delete', key, { recordId: id }, {}).catch((err) => {
118
- console.error(`[ORM] Failed to persist delete for ${key}:${id}: ${err instanceof Error ? err.message : String(err)}`);
118
+ Orm.instance.emitPersistError({
119
+ operation: 'delete',
120
+ modelName: key,
121
+ recordId: id,
122
+ error: err instanceof Error ? err : new Error(String(err)),
123
+ });
119
124
  });
120
125
  }
121
126
  if (id)
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "stonyx-async",
5
5
  "stonyx-module"
6
6
  ],
7
- "version": "0.3.2-beta.7",
7
+ "version": "0.3.2-beta.8",
8
8
  "description": "",
9
9
  "main": "dist/index.js",
10
10
  "type": "module",
@@ -61,9 +61,9 @@
61
61
  },
62
62
  "homepage": "https://github.com/abofs/stonyx-orm#readme",
63
63
  "dependencies": {
64
- "@stonyx/cron": "0.2.1-beta.29",
64
+ "@stonyx/cron": "0.2.1-beta.38",
65
65
  "@stonyx/events": "0.1.1-beta.9",
66
- "stonyx": "0.2.3-beta.11"
66
+ "stonyx": "0.2.3-beta.53"
67
67
  },
68
68
  "peerDependencies": {
69
69
  "@stonyx/rest-server": ">=0.2.1-beta.14",
@@ -83,17 +83,18 @@
83
83
  },
84
84
  "devDependencies": {
85
85
  "@stonyx/rest-server": "0.2.1-beta.30",
86
- "@stonyx/utils": "0.2.3-beta.7",
86
+ "@stonyx/utils": "0.2.3-beta.23",
87
87
  "@types/node": "^25.6.0",
88
88
  "mysql2": "^3.20.0",
89
89
  "pg": "^8.20.0",
90
90
  "qunit": "^2.24.1",
91
91
  "sinon": "^21.0.0",
92
+ "tsx": "^4.21.0",
92
93
  "typescript": "^5.8.3"
93
94
  },
94
95
  "scripts": {
95
96
  "build": "tsc",
96
97
  "build:test": "tsc -p tsconfig.test.json",
97
- "test": "npm run build && npm run build:test && stonyx test 'dist-test/test/**/*-test.js'"
98
+ "test": "pnpm build && NODE_ENV=test node --import tsx/esm --import ./test/setup.ts node_modules/qunit/bin/qunit.js 'test/**/*-test.ts'"
98
99
  }
99
100
  }
package/src/index.ts CHANGED
@@ -26,6 +26,7 @@ import { count, avg, sum, min, max } from './aggregates.js';
26
26
 
27
27
  export { default } from './main.js';
28
28
  export { store, relationships } from './main.js';
29
+ export type { PersistErrorDetail } from './main.js';
29
30
  export { Model, View, Serializer }; // base classes
30
31
  export { attr, belongsTo, hasMany, createRecord, updateRecord }; // helpers
31
32
  export { count, avg, sum, min, max }; // aggregate helpers
package/src/main.ts CHANGED
@@ -49,6 +49,13 @@ const defaultOptions: OrmOptions = {
49
49
  dbType: 'json'
50
50
  }
51
51
 
52
+ export interface PersistErrorDetail {
53
+ operation: 'create' | 'update' | 'delete';
54
+ modelName: string;
55
+ recordId: unknown;
56
+ error: Error;
57
+ }
58
+
52
59
  export default class Orm {
53
60
  static initialized: boolean = false;
54
61
  static relationships: Map<string, Map<string, unknown>> = new Map();
@@ -65,6 +72,8 @@ export default class Orm {
65
72
  sqlDb?: SqlDb;
66
73
  db?: OrmDB | SqlDb;
67
74
 
75
+ private _persistErrorHandler: ((detail: PersistErrorDetail) => void) | null = null;
76
+
68
77
  constructor(options: OrmOptions = {}) {
69
78
  if (Orm.instance) return Orm.instance;
70
79
 
@@ -214,6 +223,32 @@ export default class Orm {
214
223
  return !!this.views[`${modelClassPrefix}View`];
215
224
  }
216
225
 
226
+ /**
227
+ * Register a callback to be invoked when a fire-and-forget SQL persist fails.
228
+ * Without a handler, persist errors are logged via log.error (backwards-compatible).
229
+ */
230
+ onPersistError(handler: ((detail: PersistErrorDetail) => void) | null): void {
231
+ this._persistErrorHandler = handler;
232
+ }
233
+
234
+ /**
235
+ * Emit a persist error to the registered handler, or fall back to log.error.
236
+ */
237
+ emitPersistError(detail: PersistErrorDetail): void {
238
+ const fallbackLog = () => log.error?.(`[ORM] Failed to persist ${detail.operation} for ${detail.modelName}:${String(detail.recordId)}: ${detail.error.message}`);
239
+
240
+ if (this._persistErrorHandler) {
241
+ try {
242
+ this._persistErrorHandler(detail);
243
+ } catch (handlerError) {
244
+ fallbackLog();
245
+ log.error?.(`[ORM] onPersistError handler threw: ${handlerError instanceof Error ? handlerError.message : String(handlerError)}`);
246
+ }
247
+ } else {
248
+ fallbackLog();
249
+ }
250
+ }
251
+
217
252
  // Queue warnings to avoid the same error from being logged in the same iteration
218
253
  warn(message: string): void {
219
254
  this.warnings.add(message);
@@ -3,7 +3,6 @@ import OrmRecord from './record.js';
3
3
  import { getGlobalRegistry, getPendingRegistry, getPendingBelongsToRegistry, getBelongsToRegistry, getHasManyRegistry } from './relationships.js';
4
4
  import type Serializer from './serializer.js';
5
5
  import { isOrmRecord } from './utils.js';
6
- import log from 'stonyx/log';
7
6
 
8
7
  interface CreateRecordOptions {
9
8
  isDbRecord?: boolean;
@@ -120,7 +119,12 @@ export function createRecord(modelName: string, rawData: { [key: string]: unknow
120
119
  if (shouldPersist) {
121
120
  const response = { data: { id: record.id } };
122
121
  orm!.sqlDb!.persist('create', modelName, { rawData }, response).catch((err: unknown) => {
123
- log.error?.(`[ORM] Failed to persist create for ${modelName}:${String(record.id)}: ${err instanceof Error ? err.message : String(err)}`);
122
+ orm!.emitPersistError({
123
+ operation: 'create',
124
+ modelName,
125
+ recordId: record.id,
126
+ error: err instanceof Error ? err : new Error(String(err)),
127
+ });
124
128
  });
125
129
  }
126
130
 
@@ -148,7 +152,12 @@ export function updateRecord(record: OrmRecord, rawData: unknown, userOptions: C
148
152
  const shouldPersist = orm?.sqlDb && !options.isDbRecord && !userOptions._relationshipKey && !options._skipAutoPersist;
149
153
  if (shouldPersist && modelName) {
150
154
  orm!.sqlDb!.persist('update', modelName, { record, oldState }, {}).catch((err: unknown) => {
151
- log.error?.(`[ORM] Failed to persist update for ${modelName}:${String(record.id)}: ${err instanceof Error ? err.message : String(err)}`);
155
+ orm!.emitPersistError({
156
+ operation: 'update',
157
+ modelName,
158
+ recordId: record.id,
159
+ error: err instanceof Error ? err : new Error(String(err)),
160
+ });
152
161
  });
153
162
  }
154
163
  }
package/src/store.ts CHANGED
@@ -179,7 +179,12 @@ export default class Store {
179
179
  // Auto-persist delete to SQL
180
180
  if (id && Orm.instance?.sqlDb) {
181
181
  Orm.instance.sqlDb.persist('delete', key, { recordId: id }, {}).catch((err: unknown) => {
182
- console.error(`[ORM] Failed to persist delete for ${key}:${id}: ${err instanceof Error ? err.message : String(err)}`);
182
+ Orm.instance.emitPersistError({
183
+ operation: 'delete',
184
+ modelName: key,
185
+ recordId: id,
186
+ error: err instanceof Error ? err : new Error(String(err)),
187
+ });
183
188
  });
184
189
  }
185
190