@stonyx/orm 0.2.1-alpha.40 → 0.2.1-alpha.42
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/dist/db.js +1 -1
- package/dist/hooks.d.ts +29 -1
- package/dist/mysql/schema-introspector.d.ts +1 -1
- package/dist/mysql/schema-introspector.js +2 -36
- package/dist/orm-request.js +1 -0
- package/dist/postgres/schema-introspector.d.ts +1 -2
- package/dist/postgres/schema-introspector.js +2 -36
- package/dist/schema-helpers.d.ts +20 -0
- package/dist/schema-helpers.js +48 -0
- package/dist/store.js +3 -3
- package/dist/types/orm-types.d.ts +5 -5
- package/package.json +1 -1
- package/src/db.ts +1 -1
- package/src/hooks.ts +30 -1
- package/src/mysql/schema-introspector.ts +2 -50
- package/src/orm-request.ts +2 -16
- package/src/postgres/postgres-db.ts +1 -1
- package/src/postgres/schema-introspector.ts +2 -45
- package/src/schema-helpers.ts +59 -0
- package/src/serializer.ts +1 -1
- package/src/store.ts +3 -3
- package/src/transforms.ts +1 -1
- package/src/types/mysql2.d.ts +24 -5
- package/src/types/orm-types.ts +5 -5
- package/src/types/pg.d.ts +7 -3
- package/src/types/stonyx-rest-server.d.ts +6 -1
- package/src/types/stonyx-utils.d.ts +6 -6
package/dist/db.js
CHANGED
|
@@ -128,7 +128,7 @@ export default class DB {
|
|
|
128
128
|
await Promise.all(collectionKeys.map(async (key) => {
|
|
129
129
|
const filePath = path.join(dirPath, `${key}.json`);
|
|
130
130
|
const exists = await fileExists(filePath);
|
|
131
|
-
const data = jsonData[key] || [];
|
|
131
|
+
const data = (jsonData[key] || []);
|
|
132
132
|
if (exists)
|
|
133
133
|
await updateFile(filePath, data, { json: true });
|
|
134
134
|
else
|
package/dist/hooks.d.ts
CHANGED
|
@@ -2,7 +2,35 @@
|
|
|
2
2
|
* Middleware-based hooks registry for ORM operations.
|
|
3
3
|
* Unlike event-based hooks, middleware hooks run sequentially and can halt operations.
|
|
4
4
|
*/
|
|
5
|
-
|
|
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>;
|
|
6
34
|
/**
|
|
7
35
|
* Register a before hook middleware that runs before the operation executes.
|
|
8
36
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ForeignKeyDef, ModelSchema, ViewSchema, SnapshotEntry } from '../types/orm-types.js';
|
|
2
2
|
export declare function introspectModels(): Record<string, ModelSchema>;
|
|
3
3
|
export declare function buildTableDDL(name: string, schema: ModelSchema, allSchemas?: Record<string, ModelSchema>): string;
|
|
4
|
-
export
|
|
4
|
+
export { getTopologicalOrder } from '../schema-helpers.js';
|
|
5
5
|
export declare function introspectViews(): Record<string, ViewSchema>;
|
|
6
6
|
export declare function buildViewDDL(name: string, viewSchema: ViewSchema, modelSchemas?: Record<string, ModelSchema>): string;
|
|
7
7
|
export declare function viewSchemasToSnapshot(viewSchemas: Record<string, ViewSchema>): Record<string, ViewSnapshotEntry>;
|
|
@@ -4,21 +4,8 @@ import { camelCaseToKebabCase } from '@stonyx/utils/string';
|
|
|
4
4
|
import { getPluralName } from '../plural-registry.js';
|
|
5
5
|
import { dbKey } from '../db.js';
|
|
6
6
|
import { AggregateProperty } from '../aggregates.js';
|
|
7
|
+
import { getRelationshipInfo, sanitizeTableName } from '../schema-helpers.js';
|
|
7
8
|
import ModelProperty from '../model-property.js';
|
|
8
|
-
function getRelationshipInfo(property) {
|
|
9
|
-
if (typeof property !== 'function')
|
|
10
|
-
return null;
|
|
11
|
-
const relType = property.__relationshipType;
|
|
12
|
-
const modelName = property.__relatedModelName || null;
|
|
13
|
-
if (relType === 'belongsTo')
|
|
14
|
-
return { type: 'belongsTo', modelName };
|
|
15
|
-
if (relType === 'hasMany')
|
|
16
|
-
return { type: 'hasMany', modelName };
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
function sanitizeTableName(name) {
|
|
20
|
-
return name.replace(/[-/]/g, '_');
|
|
21
|
-
}
|
|
22
9
|
export function introspectModels() {
|
|
23
10
|
const { models } = Orm.instance;
|
|
24
11
|
const schemas = {};
|
|
@@ -112,28 +99,7 @@ function getReferencedIdType(tableName, allSchemas) {
|
|
|
112
99
|
// Default to INT if referenced table not found in schemas
|
|
113
100
|
return 'INT';
|
|
114
101
|
}
|
|
115
|
-
export
|
|
116
|
-
const visited = new Set();
|
|
117
|
-
const order = [];
|
|
118
|
-
function visit(name) {
|
|
119
|
-
if (visited.has(name))
|
|
120
|
-
return;
|
|
121
|
-
visited.add(name);
|
|
122
|
-
const schema = schemas[name];
|
|
123
|
-
if (!schema)
|
|
124
|
-
return;
|
|
125
|
-
// Visit dependencies (belongsTo targets) first
|
|
126
|
-
for (const targetModelName of Object.values(schema.relationships.belongsTo)) {
|
|
127
|
-
if (targetModelName)
|
|
128
|
-
visit(targetModelName);
|
|
129
|
-
}
|
|
130
|
-
order.push(name);
|
|
131
|
-
}
|
|
132
|
-
for (const name of Object.keys(schemas)) {
|
|
133
|
-
visit(name);
|
|
134
|
-
}
|
|
135
|
-
return order;
|
|
136
|
-
}
|
|
102
|
+
export { getTopologicalOrder } from '../schema-helpers.js';
|
|
137
103
|
export function introspectViews() {
|
|
138
104
|
const orm = Orm.instance;
|
|
139
105
|
if (!orm.views)
|
package/dist/orm-request.js
CHANGED
|
@@ -21,9 +21,8 @@ export declare function buildTableDDL(name: string, schema: ModelSchema, allSche
|
|
|
21
21
|
* Build HNSW index DDL for vector columns on a model.
|
|
22
22
|
*/
|
|
23
23
|
export declare function buildVectorIndexDDL(name: string, schema: ModelSchema): string[];
|
|
24
|
-
export
|
|
24
|
+
export { getTopologicalOrder } from '../schema-helpers.js';
|
|
25
25
|
export declare function introspectViews(): Record<string, ViewSchema>;
|
|
26
26
|
export declare function buildViewDDL(name: string, viewSchema: ViewSchema, modelSchemas?: Record<string, ModelSchema>): string;
|
|
27
27
|
export declare function viewSchemasToSnapshot(viewSchemas: Record<string, ViewSchema>): Record<string, ViewSnapshotEntry>;
|
|
28
28
|
export declare function schemasToSnapshot(schemas: Record<string, ModelSchema>): Record<string, ModelSnapshotEntry>;
|
|
29
|
-
export {};
|
|
@@ -4,21 +4,8 @@ import { camelCaseToKebabCase } from '@stonyx/utils/string';
|
|
|
4
4
|
import { getPluralName } from '../plural-registry.js';
|
|
5
5
|
import { dbKey } from '../db.js';
|
|
6
6
|
import { AggregateProperty } from '../aggregates.js';
|
|
7
|
+
import { getRelationshipInfo, sanitizeTableName } from '../schema-helpers.js';
|
|
7
8
|
import ModelProperty from '../model-property.js';
|
|
8
|
-
function getRelationshipInfo(property) {
|
|
9
|
-
if (typeof property !== 'function')
|
|
10
|
-
return null;
|
|
11
|
-
const relType = property.__relationshipType;
|
|
12
|
-
const modelName = property.__relatedModelName || null;
|
|
13
|
-
if (relType === 'belongsTo')
|
|
14
|
-
return { type: 'belongsTo', modelName };
|
|
15
|
-
if (relType === 'hasMany')
|
|
16
|
-
return { type: 'hasMany', modelName };
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
function sanitizeTableName(name) {
|
|
20
|
-
return name.replace(/[-/]/g, '_');
|
|
21
|
-
}
|
|
22
9
|
export function introspectModels() {
|
|
23
10
|
const { models } = Orm.instance;
|
|
24
11
|
const schemas = {};
|
|
@@ -129,28 +116,7 @@ function getReferencedIdType(tableName, allSchemas) {
|
|
|
129
116
|
}
|
|
130
117
|
return 'INTEGER';
|
|
131
118
|
}
|
|
132
|
-
export
|
|
133
|
-
const visited = new Set();
|
|
134
|
-
const order = [];
|
|
135
|
-
function visit(name) {
|
|
136
|
-
if (visited.has(name))
|
|
137
|
-
return;
|
|
138
|
-
visited.add(name);
|
|
139
|
-
const schema = schemas[name];
|
|
140
|
-
if (!schema)
|
|
141
|
-
return;
|
|
142
|
-
// Visit dependencies (belongsTo targets) first
|
|
143
|
-
for (const targetModelName of Object.values(schema.relationships.belongsTo)) {
|
|
144
|
-
if (targetModelName)
|
|
145
|
-
visit(targetModelName);
|
|
146
|
-
}
|
|
147
|
-
order.push(name);
|
|
148
|
-
}
|
|
149
|
-
for (const name of Object.keys(schemas)) {
|
|
150
|
-
visit(name);
|
|
151
|
-
}
|
|
152
|
-
return order;
|
|
153
|
-
}
|
|
119
|
+
export { getTopologicalOrder } from '../schema-helpers.js';
|
|
154
120
|
export function introspectViews() {
|
|
155
121
|
const orm = Orm.instance;
|
|
156
122
|
if (!orm.views)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ModelSchema } from './types/orm-types.js';
|
|
2
|
+
export interface SchemaRelationshipInfo {
|
|
3
|
+
type: 'belongsTo' | 'hasMany';
|
|
4
|
+
modelName: string | null;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Detect relationship type and target model from a model property function.
|
|
8
|
+
* Returns null if the property is not a relationship.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getRelationshipInfo(property: unknown): SchemaRelationshipInfo | null;
|
|
11
|
+
/**
|
|
12
|
+
* Sanitize a model/table name for use in SQL identifiers.
|
|
13
|
+
* Replaces hyphens and slashes with underscores.
|
|
14
|
+
*/
|
|
15
|
+
export declare function sanitizeTableName(name: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Sort model schemas in dependency order (belongsTo targets before dependents).
|
|
18
|
+
* Uses depth-first traversal to ensure referenced tables are created first.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getTopologicalOrder(schemas: Record<string, ModelSchema>): string[];
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect relationship type and target model from a model property function.
|
|
3
|
+
* Returns null if the property is not a relationship.
|
|
4
|
+
*/
|
|
5
|
+
export function getRelationshipInfo(property) {
|
|
6
|
+
if (typeof property !== 'function')
|
|
7
|
+
return null;
|
|
8
|
+
const relType = property.__relationshipType;
|
|
9
|
+
const modelName = property.__relatedModelName || null;
|
|
10
|
+
if (relType === 'belongsTo')
|
|
11
|
+
return { type: 'belongsTo', modelName };
|
|
12
|
+
if (relType === 'hasMany')
|
|
13
|
+
return { type: 'hasMany', modelName };
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Sanitize a model/table name for use in SQL identifiers.
|
|
18
|
+
* Replaces hyphens and slashes with underscores.
|
|
19
|
+
*/
|
|
20
|
+
export function sanitizeTableName(name) {
|
|
21
|
+
return name.replace(/[-/]/g, '_');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Sort model schemas in dependency order (belongsTo targets before dependents).
|
|
25
|
+
* Uses depth-first traversal to ensure referenced tables are created first.
|
|
26
|
+
*/
|
|
27
|
+
export function getTopologicalOrder(schemas) {
|
|
28
|
+
const visited = new Set();
|
|
29
|
+
const order = [];
|
|
30
|
+
function visit(name) {
|
|
31
|
+
if (visited.has(name))
|
|
32
|
+
return;
|
|
33
|
+
visited.add(name);
|
|
34
|
+
const schema = schemas[name];
|
|
35
|
+
if (!schema)
|
|
36
|
+
return;
|
|
37
|
+
// Visit dependencies (belongsTo targets) first
|
|
38
|
+
for (const targetModelName of Object.values(schema.relationships.belongsTo)) {
|
|
39
|
+
if (targetModelName)
|
|
40
|
+
visit(targetModelName);
|
|
41
|
+
}
|
|
42
|
+
order.push(name);
|
|
43
|
+
}
|
|
44
|
+
for (const name of Object.keys(schemas)) {
|
|
45
|
+
visit(name);
|
|
46
|
+
}
|
|
47
|
+
return order;
|
|
48
|
+
}
|
package/dist/store.js
CHANGED
|
@@ -119,14 +119,14 @@ export default class Store {
|
|
|
119
119
|
unloadRecord(model, id, options = {}) {
|
|
120
120
|
const modelStore = this.data.get(model);
|
|
121
121
|
if (!modelStore) {
|
|
122
|
-
console.warn(`[Store] Cannot unload record: model "${model}" not found in store`);
|
|
122
|
+
console.warn(`[Store] Cannot unload record: model "${model}" not found in store — ensure the model is registered before unloading`);
|
|
123
123
|
return;
|
|
124
124
|
}
|
|
125
125
|
if (typeof id !== 'string' && typeof id !== 'number')
|
|
126
126
|
return;
|
|
127
127
|
const raw = modelStore.get(id);
|
|
128
128
|
if (!raw || !isStoreRecord(raw)) {
|
|
129
|
-
console.warn(`[Store] Cannot unload record: ${model}:${id} not found in store`);
|
|
129
|
+
console.warn(`[Store] Cannot unload record: ${model}:${id} not found in store — it may have already been unloaded`);
|
|
130
130
|
return;
|
|
131
131
|
}
|
|
132
132
|
const record = raw;
|
|
@@ -145,7 +145,7 @@ export default class Store {
|
|
|
145
145
|
unloadAllRecords(model, options = {}) {
|
|
146
146
|
const modelStore = this.data.get(model);
|
|
147
147
|
if (!modelStore) {
|
|
148
|
-
console.warn(`[Store] Cannot unload all records: model "${model}" not found in store`);
|
|
148
|
+
console.warn(`[Store] Cannot unload all records: model "${model}" not found in store — ensure the model is registered before unloading`);
|
|
149
149
|
return;
|
|
150
150
|
}
|
|
151
151
|
const recordIds = Array.from(modelStore.keys());
|
|
@@ -5,7 +5,7 @@ export interface OrmDbConfig {
|
|
|
5
5
|
mode: string;
|
|
6
6
|
directory: string;
|
|
7
7
|
autosave: string;
|
|
8
|
-
saveInterval:
|
|
8
|
+
saveInterval: string | number;
|
|
9
9
|
}
|
|
10
10
|
export interface OrmMysqlConfig {
|
|
11
11
|
host: string;
|
|
@@ -63,23 +63,23 @@ export interface SourceRecord {
|
|
|
63
63
|
};
|
|
64
64
|
__data?: Record<string, unknown>;
|
|
65
65
|
__relationships?: Record<string, unknown>;
|
|
66
|
-
id:
|
|
66
|
+
id: string | number;
|
|
67
67
|
[key: string]: unknown;
|
|
68
68
|
}
|
|
69
69
|
export interface OrmRecord {
|
|
70
|
-
id: string | number
|
|
70
|
+
id: string | number;
|
|
71
71
|
__model?: {
|
|
72
72
|
__name: string;
|
|
73
73
|
};
|
|
74
74
|
__data: Record<string, unknown> & {
|
|
75
|
-
id?:
|
|
75
|
+
id?: string | number;
|
|
76
76
|
__pendingSqlId?: boolean;
|
|
77
77
|
};
|
|
78
78
|
__relationships: Record<string, unknown>;
|
|
79
79
|
toJSON?(options?: {
|
|
80
80
|
fields?: Set<string>;
|
|
81
81
|
baseUrl?: string;
|
|
82
|
-
}): unknown
|
|
82
|
+
}): Record<string, unknown>;
|
|
83
83
|
[key: string]: unknown;
|
|
84
84
|
}
|
|
85
85
|
export interface ForeignKeyDef {
|
package/package.json
CHANGED
package/src/db.ts
CHANGED
|
@@ -167,7 +167,7 @@ export default class DB {
|
|
|
167
167
|
await Promise.all(collectionKeys.map(async key => {
|
|
168
168
|
const filePath = path.join(dirPath, `${key}.json`);
|
|
169
169
|
const exists = await fileExists(filePath);
|
|
170
|
-
const data = jsonData[key] || [];
|
|
170
|
+
const data = (jsonData[key] || []) as Record<string, unknown> | unknown[];
|
|
171
171
|
|
|
172
172
|
if (exists) await updateFile(filePath, data, { json: true });
|
|
173
173
|
else await createFile(filePath, data, { json: true });
|
package/src/hooks.ts
CHANGED
|
@@ -19,7 +19,36 @@
|
|
|
19
19
|
* Unlike event-based hooks, middleware hooks run sequentially and can halt operations.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
/** Context object passed to before/after hook handlers. */
|
|
23
|
+
export interface HookContext {
|
|
24
|
+
/** Model name (e.g. 'user', 'animal'). */
|
|
25
|
+
model: string;
|
|
26
|
+
/** Operation name: 'create', 'update', 'delete', 'get', or 'list'. */
|
|
27
|
+
operation: string;
|
|
28
|
+
/** The incoming HTTP request object. */
|
|
29
|
+
request?: unknown;
|
|
30
|
+
/** URL route parameters (e.g. { id: '42' }). */
|
|
31
|
+
params?: Record<string, string>;
|
|
32
|
+
/** Parsed request body for create/update operations. */
|
|
33
|
+
body?: Record<string, unknown>;
|
|
34
|
+
/** URL query string parameters. */
|
|
35
|
+
query?: Record<string, string>;
|
|
36
|
+
/** Mutable state bag shared across hooks within a single request. */
|
|
37
|
+
state?: Record<string, unknown>;
|
|
38
|
+
/** Previous record state (available in update hooks). */
|
|
39
|
+
oldState?: unknown;
|
|
40
|
+
/** Target record ID for single-record operations. */
|
|
41
|
+
recordId?: string | number;
|
|
42
|
+
/** Response data (available in after hooks). */
|
|
43
|
+
response?: unknown;
|
|
44
|
+
/** The affected record (available in after hooks for create/update/delete). */
|
|
45
|
+
record?: unknown;
|
|
46
|
+
/** The affected records (available in after hooks for list operations). */
|
|
47
|
+
records?: unknown[];
|
|
48
|
+
[key: string]: unknown;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type HookHandler = (context: HookContext) => unknown | Promise<unknown>;
|
|
23
52
|
|
|
24
53
|
// Map of "operation:model" -> handler[]
|
|
25
54
|
const beforeHooks: Map<string, HookHandler[]> = new Map();
|
|
@@ -4,39 +4,15 @@ import { camelCaseToKebabCase } from '@stonyx/utils/string';
|
|
|
4
4
|
import { getPluralName } from '../plural-registry.js';
|
|
5
5
|
import { dbKey } from '../db.js';
|
|
6
6
|
import { AggregateProperty } from '../aggregates.js';
|
|
7
|
+
import { getRelationshipInfo, sanitizeTableName } from '../schema-helpers.js';
|
|
7
8
|
import type { ForeignKeyDef, ModelSchema, ViewSchema, SnapshotEntry } from '../types/orm-types.js';
|
|
8
9
|
import ModelProperty from '../model-property.js';
|
|
9
10
|
|
|
10
|
-
interface RelationshipInfo {
|
|
11
|
-
type: 'belongsTo' | 'hasMany';
|
|
12
|
-
modelName: string | null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
11
|
interface JoinClause {
|
|
16
12
|
table: string;
|
|
17
13
|
condition: string;
|
|
18
14
|
}
|
|
19
15
|
|
|
20
|
-
interface RelationshipProperty {
|
|
21
|
-
__relatedModelName?: string | null;
|
|
22
|
-
__relationshipType?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function getRelationshipInfo(property: unknown): RelationshipInfo | null {
|
|
26
|
-
if (typeof property !== 'function') return null;
|
|
27
|
-
const relType = (property as RelationshipProperty).__relationshipType;
|
|
28
|
-
const modelName = (property as RelationshipProperty).__relatedModelName || null;
|
|
29
|
-
|
|
30
|
-
if (relType === 'belongsTo') return { type: 'belongsTo', modelName };
|
|
31
|
-
if (relType === 'hasMany') return { type: 'hasMany', modelName };
|
|
32
|
-
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function sanitizeTableName(name: string): string {
|
|
37
|
-
return name.replace(/[-/]/g, '_');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
16
|
export function introspectModels(): Record<string, ModelSchema> {
|
|
41
17
|
const { models } = (Orm as unknown as { instance: { models: Record<string, unknown>; transforms: Record<string, unknown> } }).instance;
|
|
42
18
|
const schemas: Record<string, ModelSchema> = {};
|
|
@@ -143,31 +119,7 @@ function getReferencedIdType(tableName: string, allSchemas: Record<string, Model
|
|
|
143
119
|
return 'INT';
|
|
144
120
|
}
|
|
145
121
|
|
|
146
|
-
export
|
|
147
|
-
const visited = new Set<string>();
|
|
148
|
-
const order: string[] = [];
|
|
149
|
-
|
|
150
|
-
function visit(name: string): void {
|
|
151
|
-
if (visited.has(name)) return;
|
|
152
|
-
visited.add(name);
|
|
153
|
-
|
|
154
|
-
const schema = schemas[name];
|
|
155
|
-
if (!schema) return;
|
|
156
|
-
|
|
157
|
-
// Visit dependencies (belongsTo targets) first
|
|
158
|
-
for (const targetModelName of Object.values(schema.relationships.belongsTo)) {
|
|
159
|
-
if (targetModelName) visit(targetModelName);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
order.push(name);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
for (const name of Object.keys(schemas)) {
|
|
166
|
-
visit(name);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return order;
|
|
170
|
-
}
|
|
122
|
+
export { getTopologicalOrder } from '../schema-helpers.js';
|
|
171
123
|
|
|
172
124
|
export function introspectViews(): Record<string, ViewSchema> {
|
|
173
125
|
const orm = (Orm as unknown as { instance: { views?: Record<string, unknown>; transforms: Record<string, unknown> } }).instance;
|
package/src/orm-request.ts
CHANGED
|
@@ -3,6 +3,7 @@ import Orm, { store, createRecord, updateRecord } from '@stonyx/orm';
|
|
|
3
3
|
import { camelCaseToKebabCase } from '@stonyx/utils/string';
|
|
4
4
|
import { getPluralName } from './plural-registry.js';
|
|
5
5
|
import { getBeforeHooks, getAfterHooks } from './hooks.js';
|
|
6
|
+
import type { HookContext } from './hooks.js';
|
|
6
7
|
import config from 'stonyx/config';
|
|
7
8
|
import type { OrmRecord } from './types/orm-types.js';
|
|
8
9
|
import { isOrmRecord } from './utils.js';
|
|
@@ -26,22 +27,6 @@ interface Filter {
|
|
|
26
27
|
value: string;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
interface HookContext {
|
|
30
|
-
model: string;
|
|
31
|
-
operation: string;
|
|
32
|
-
request: OrmRequest$;
|
|
33
|
-
params: { [key: string]: string };
|
|
34
|
-
body?: { [key: string]: unknown };
|
|
35
|
-
query?: { [key: string]: string };
|
|
36
|
-
state: { [key: string]: unknown };
|
|
37
|
-
oldState?: unknown;
|
|
38
|
-
recordId?: string | number;
|
|
39
|
-
response?: unknown;
|
|
40
|
-
record?: unknown;
|
|
41
|
-
records?: unknown;
|
|
42
|
-
[key: string]: unknown;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
30
|
interface JsonApiResponse {
|
|
46
31
|
data: unknown;
|
|
47
32
|
links?: { [key: string]: string };
|
|
@@ -592,5 +577,6 @@ export default class OrmRequest extends Request {
|
|
|
592
577
|
if (!access) return 403;
|
|
593
578
|
if (Array.isArray(access) && !access.includes(methodAccessMap[request.method])) return 403;
|
|
594
579
|
if (typeof access === 'function') state.filter = access;
|
|
580
|
+
return undefined;
|
|
595
581
|
}
|
|
596
582
|
}
|
|
@@ -507,7 +507,7 @@ export default class PostgresDB {
|
|
|
507
507
|
// Re-key the record in the store if PostgreSQL generated the ID (via RETURNING)
|
|
508
508
|
if (isPendingId && result.rows.length > 0) {
|
|
509
509
|
const pendingId = record.id;
|
|
510
|
-
const realId = result.rows[0].id;
|
|
510
|
+
const realId = result.rows[0].id as string | number;
|
|
511
511
|
const modelStore = (this.deps.store as unknown as { get(name: string): Map<unknown, unknown> }).get(modelName);
|
|
512
512
|
|
|
513
513
|
modelStore.delete(pendingId);
|
|
@@ -4,14 +4,10 @@ import { camelCaseToKebabCase } from '@stonyx/utils/string';
|
|
|
4
4
|
import { getPluralName } from '../plural-registry.js';
|
|
5
5
|
import { dbKey } from '../db.js';
|
|
6
6
|
import { AggregateProperty } from '../aggregates.js';
|
|
7
|
+
import { getRelationshipInfo, sanitizeTableName } from '../schema-helpers.js';
|
|
7
8
|
import type { ForeignKeyDef, ModelSchema, ViewSchema } from '../types/orm-types.js';
|
|
8
9
|
import ModelProperty from '../model-property.js';
|
|
9
10
|
|
|
10
|
-
interface RelationshipInfo {
|
|
11
|
-
type: 'belongsTo' | 'hasMany';
|
|
12
|
-
modelName: string | null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
11
|
interface ViewSnapshotEntry {
|
|
16
12
|
viewName: string;
|
|
17
13
|
source: string;
|
|
@@ -35,21 +31,6 @@ interface JoinDef {
|
|
|
35
31
|
condition: string;
|
|
36
32
|
}
|
|
37
33
|
|
|
38
|
-
function getRelationshipInfo(property: unknown): RelationshipInfo | null {
|
|
39
|
-
if (typeof property !== 'function') return null;
|
|
40
|
-
const relType = (property as { __relationshipType?: string }).__relationshipType;
|
|
41
|
-
const modelName = (property as { __relatedModelName?: string }).__relatedModelName || null;
|
|
42
|
-
|
|
43
|
-
if (relType === 'belongsTo') return { type: 'belongsTo', modelName };
|
|
44
|
-
if (relType === 'hasMany') return { type: 'hasMany', modelName };
|
|
45
|
-
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function sanitizeTableName(name: string): string {
|
|
50
|
-
return name.replace(/[-/]/g, '_');
|
|
51
|
-
}
|
|
52
|
-
|
|
53
34
|
export function introspectModels(): Record<string, ModelSchema> {
|
|
54
35
|
const { models } = Orm.instance as { models: Record<string, unknown> };
|
|
55
36
|
const schemas: Record<string, ModelSchema> = {};
|
|
@@ -177,31 +158,7 @@ function getReferencedIdType(tableName: string, allSchemas: Record<string, Model
|
|
|
177
158
|
return 'INTEGER';
|
|
178
159
|
}
|
|
179
160
|
|
|
180
|
-
export
|
|
181
|
-
const visited = new Set<string>();
|
|
182
|
-
const order: string[] = [];
|
|
183
|
-
|
|
184
|
-
function visit(name: string): void {
|
|
185
|
-
if (visited.has(name)) return;
|
|
186
|
-
visited.add(name);
|
|
187
|
-
|
|
188
|
-
const schema = schemas[name];
|
|
189
|
-
if (!schema) return;
|
|
190
|
-
|
|
191
|
-
// Visit dependencies (belongsTo targets) first
|
|
192
|
-
for (const targetModelName of Object.values(schema.relationships.belongsTo)) {
|
|
193
|
-
if (targetModelName) visit(targetModelName);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
order.push(name);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
for (const name of Object.keys(schemas)) {
|
|
200
|
-
visit(name);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return order;
|
|
204
|
-
}
|
|
161
|
+
export { getTopologicalOrder } from '../schema-helpers.js';
|
|
205
162
|
|
|
206
163
|
export function introspectViews(): Record<string, ViewSchema> {
|
|
207
164
|
const orm = Orm.instance as { views?: Record<string, unknown> };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ModelSchema } from './types/orm-types.js';
|
|
2
|
+
|
|
3
|
+
export interface SchemaRelationshipInfo {
|
|
4
|
+
type: 'belongsTo' | 'hasMany';
|
|
5
|
+
modelName: string | null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Detect relationship type and target model from a model property function.
|
|
10
|
+
* Returns null if the property is not a relationship.
|
|
11
|
+
*/
|
|
12
|
+
export function getRelationshipInfo(property: unknown): SchemaRelationshipInfo | null {
|
|
13
|
+
if (typeof property !== 'function') return null;
|
|
14
|
+
const relType = (property as { __relationshipType?: string }).__relationshipType;
|
|
15
|
+
const modelName = (property as { __relatedModelName?: string }).__relatedModelName || null;
|
|
16
|
+
|
|
17
|
+
if (relType === 'belongsTo') return { type: 'belongsTo', modelName };
|
|
18
|
+
if (relType === 'hasMany') return { type: 'hasMany', modelName };
|
|
19
|
+
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Sanitize a model/table name for use in SQL identifiers.
|
|
25
|
+
* Replaces hyphens and slashes with underscores.
|
|
26
|
+
*/
|
|
27
|
+
export function sanitizeTableName(name: string): string {
|
|
28
|
+
return name.replace(/[-/]/g, '_');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Sort model schemas in dependency order (belongsTo targets before dependents).
|
|
33
|
+
* Uses depth-first traversal to ensure referenced tables are created first.
|
|
34
|
+
*/
|
|
35
|
+
export function getTopologicalOrder(schemas: Record<string, ModelSchema>): string[] {
|
|
36
|
+
const visited = new Set<string>();
|
|
37
|
+
const order: string[] = [];
|
|
38
|
+
|
|
39
|
+
function visit(name: string): void {
|
|
40
|
+
if (visited.has(name)) return;
|
|
41
|
+
visited.add(name);
|
|
42
|
+
|
|
43
|
+
const schema = schemas[name];
|
|
44
|
+
if (!schema) return;
|
|
45
|
+
|
|
46
|
+
// Visit dependencies (belongsTo targets) first
|
|
47
|
+
for (const targetModelName of Object.values(schema.relationships.belongsTo)) {
|
|
48
|
+
if (targetModelName) visit(targetModelName);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
order.push(name);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const name of Object.keys(schemas)) {
|
|
55
|
+
visit(name);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return order;
|
|
59
|
+
}
|
package/src/serializer.ts
CHANGED
|
@@ -33,7 +33,7 @@ function query(rawData: unknown, pathPrefix: string, subPath: unknown): unknown
|
|
|
33
33
|
|
|
34
34
|
const [path, getter, pointer] = makeArray(subPath) as [string, unknown, string | undefined];
|
|
35
35
|
const fullPath = `${pathPrefix}${path}`;
|
|
36
|
-
const value = get(rawData, fullPath);
|
|
36
|
+
const value = get(rawData as Record<string, unknown>, fullPath);
|
|
37
37
|
|
|
38
38
|
if (getter === undefined || getter === null) return value;
|
|
39
39
|
|
package/src/store.ts
CHANGED
|
@@ -185,14 +185,14 @@ export default class Store {
|
|
|
185
185
|
const modelStore = this.data.get(model);
|
|
186
186
|
|
|
187
187
|
if (!modelStore) {
|
|
188
|
-
console.warn(`[Store] Cannot unload record: model "${model}" not found in store`);
|
|
188
|
+
console.warn(`[Store] Cannot unload record: model "${model}" not found in store — ensure the model is registered before unloading`);
|
|
189
189
|
return;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
if (typeof id !== 'string' && typeof id !== 'number') return;
|
|
193
193
|
const raw = modelStore.get(id);
|
|
194
194
|
if (!raw || !isStoreRecord(raw)) {
|
|
195
|
-
console.warn(`[Store] Cannot unload record: ${model}:${id} not found in store`);
|
|
195
|
+
console.warn(`[Store] Cannot unload record: ${model}:${id} not found in store — it may have already been unloaded`);
|
|
196
196
|
return;
|
|
197
197
|
}
|
|
198
198
|
const record = raw;
|
|
@@ -217,7 +217,7 @@ export default class Store {
|
|
|
217
217
|
const modelStore = this.data.get(model);
|
|
218
218
|
|
|
219
219
|
if (!modelStore) {
|
|
220
|
-
console.warn(`[Store] Cannot unload all records: model "${model}" not found in store`);
|
|
220
|
+
console.warn(`[Store] Cannot unload all records: model "${model}" not found in store — ensure the model is registered before unloading`);
|
|
221
221
|
return;
|
|
222
222
|
}
|
|
223
223
|
|
package/src/transforms.ts
CHANGED
|
@@ -7,7 +7,7 @@ const transforms: Record<string, (value: unknown) => unknown> = {
|
|
|
7
7
|
number: (value: unknown) => parseInt(value as string),
|
|
8
8
|
passthrough: (value: unknown) => value,
|
|
9
9
|
string: (value: unknown) => String(value),
|
|
10
|
-
timestamp: (value: unknown) => getTimestamp(value),
|
|
10
|
+
timestamp: (value: unknown) => getTimestamp(value as string | number | Date | undefined),
|
|
11
11
|
trim: (value: unknown) => (value as string)?.trim(),
|
|
12
12
|
uppercase: (value: unknown) => (value as string)?.toUpperCase(),
|
|
13
13
|
};
|
package/src/types/mysql2.d.ts
CHANGED
|
@@ -5,16 +5,35 @@ declare module 'mysql2/promise' {
|
|
|
5
5
|
password: string;
|
|
6
6
|
database: string;
|
|
7
7
|
port?: number;
|
|
8
|
-
|
|
8
|
+
waitForConnections?: boolean;
|
|
9
|
+
connectionLimit?: number;
|
|
10
|
+
queueLimit?: number;
|
|
11
|
+
enableKeepAlive?: boolean;
|
|
12
|
+
keepAliveInitialDelay?: number;
|
|
13
|
+
[key: string]: string | number | boolean | undefined;
|
|
9
14
|
}
|
|
10
15
|
|
|
11
|
-
interface
|
|
12
|
-
|
|
16
|
+
interface ExecuteResult {
|
|
17
|
+
insertId: number;
|
|
18
|
+
affectedRows: number;
|
|
19
|
+
changedRows: number;
|
|
20
|
+
fieldCount: number;
|
|
21
|
+
info: string;
|
|
22
|
+
serverStatus: number;
|
|
23
|
+
warningStatus: number;
|
|
13
24
|
}
|
|
14
25
|
|
|
26
|
+
interface FieldPacket {
|
|
27
|
+
name: string;
|
|
28
|
+
type: number;
|
|
29
|
+
length: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type RowDataPacket = Record<string, string | number | boolean | null>;
|
|
33
|
+
|
|
15
34
|
interface Pool {
|
|
16
|
-
execute(sql: string, params?: unknown[]): Promise<[
|
|
17
|
-
query(sql: string, params?: unknown[]): Promise<[
|
|
35
|
+
execute(sql: string, params?: unknown[]): Promise<[RowDataPacket[] | ExecuteResult, FieldPacket[]]>;
|
|
36
|
+
query(sql: string, params?: unknown[]): Promise<[RowDataPacket[] | ExecuteResult, FieldPacket[]]>;
|
|
18
37
|
end(): Promise<void>;
|
|
19
38
|
getConnection(): Promise<PoolConnection>;
|
|
20
39
|
}
|
package/src/types/orm-types.ts
CHANGED
|
@@ -6,7 +6,7 @@ export interface OrmDbConfig {
|
|
|
6
6
|
mode: string;
|
|
7
7
|
directory: string;
|
|
8
8
|
autosave: string;
|
|
9
|
-
saveInterval:
|
|
9
|
+
saveInterval: string | number;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export interface OrmMysqlConfig {
|
|
@@ -68,16 +68,16 @@ export interface SourceRecord {
|
|
|
68
68
|
__model: { __name: string; [key: string]: unknown };
|
|
69
69
|
__data?: Record<string, unknown>;
|
|
70
70
|
__relationships?: Record<string, unknown>;
|
|
71
|
-
id:
|
|
71
|
+
id: string | number;
|
|
72
72
|
[key: string]: unknown;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
export interface OrmRecord {
|
|
76
|
-
id: string | number
|
|
76
|
+
id: string | number;
|
|
77
77
|
__model?: { __name: string };
|
|
78
|
-
__data: Record<string, unknown> & { id?:
|
|
78
|
+
__data: Record<string, unknown> & { id?: string | number; __pendingSqlId?: boolean };
|
|
79
79
|
__relationships: Record<string, unknown>;
|
|
80
|
-
toJSON?(options?: { fields?: Set<string>; baseUrl?: string }): unknown
|
|
80
|
+
toJSON?(options?: { fields?: Set<string>; baseUrl?: string }): Record<string, unknown>;
|
|
81
81
|
[key: string]: unknown;
|
|
82
82
|
}
|
|
83
83
|
|
package/src/types/pg.d.ts
CHANGED
|
@@ -5,13 +5,17 @@ declare module 'pg' {
|
|
|
5
5
|
password?: string;
|
|
6
6
|
database?: string;
|
|
7
7
|
port?: number;
|
|
8
|
-
|
|
8
|
+
max?: number;
|
|
9
|
+
idleTimeoutMillis?: number;
|
|
10
|
+
connectionTimeoutMillis?: number;
|
|
9
11
|
}
|
|
10
12
|
|
|
13
|
+
type RowData = Record<string, string | number | boolean | null>;
|
|
14
|
+
|
|
11
15
|
interface QueryResult {
|
|
12
|
-
rows:
|
|
16
|
+
rows: RowData[];
|
|
13
17
|
rowCount: number;
|
|
14
|
-
fields?: { name: string }[];
|
|
18
|
+
fields?: { name: string; dataTypeID: number }[];
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
export class Pool {
|
|
@@ -3,9 +3,14 @@ declare module '@stonyx/rest-server' {
|
|
|
3
3
|
constructor(...args: unknown[]);
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
interface RouteOptions {
|
|
7
|
+
name: string;
|
|
8
|
+
options?: { model: string; access: (request: unknown) => unknown } | Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
export default class RestServer {
|
|
7
12
|
static instance: RestServer;
|
|
8
13
|
static close(): void;
|
|
9
|
-
mountRoute(RequestClass:
|
|
14
|
+
mountRoute(RequestClass: typeof Request, options: RouteOptions): void;
|
|
10
15
|
}
|
|
11
16
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
declare module '@stonyx/utils/file' {
|
|
2
|
-
export function createFile(path: string, data: unknown, options?: { json?: boolean }): Promise<void>;
|
|
2
|
+
export function createFile(path: string, data: string | Record<string, unknown> | unknown[], options?: { json?: boolean }): Promise<void>;
|
|
3
3
|
export function createDirectory(path: string): Promise<void>;
|
|
4
|
-
export function updateFile(path: string, data: unknown, options?: { json?: boolean }): Promise<void>;
|
|
5
|
-
export function readFile(path: string, options?: { json?: boolean; missingFileCallback?: () => Promise<unknown
|
|
4
|
+
export function updateFile(path: string, data: string | Record<string, unknown> | unknown[], options?: { json?: boolean }): Promise<void>;
|
|
5
|
+
export function readFile(path: string, options?: { json?: boolean; missingFileCallback?: () => Promise<Record<string, unknown>> }): Promise<string | Record<string, unknown> | unknown[]>;
|
|
6
6
|
export function fileExists(path: string): Promise<boolean>;
|
|
7
7
|
export function deleteDirectory(path: string): Promise<void>;
|
|
8
8
|
export function forEachFileImport(
|
|
9
9
|
path: string,
|
|
10
|
-
callback: (exported:
|
|
10
|
+
callback: (exported: Function | Record<string, unknown>, meta: { name: string }) => void | unknown,
|
|
11
11
|
options?: { ignoreAccessFailure?: boolean; rawName?: boolean; recursive?: boolean; recursiveNaming?: boolean }
|
|
12
12
|
): Promise<void>;
|
|
13
13
|
}
|
|
@@ -19,13 +19,13 @@ declare module '@stonyx/utils/string' {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
declare module '@stonyx/utils/object' {
|
|
22
|
-
export function get(obj:
|
|
22
|
+
export function get(obj: Record<string, unknown>, path: string): unknown;
|
|
23
23
|
export function getOrSet<T>(map: Map<unknown, T>, key: unknown, defaultValue: T): T;
|
|
24
24
|
export function makeArray<T>(value: T | T[]): T[];
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
declare module '@stonyx/utils/date' {
|
|
28
|
-
export function getTimestamp(value?:
|
|
28
|
+
export function getTimestamp(value?: string | number | Date): number;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
declare module '@stonyx/utils/prompt' {
|