@stonyx/orm 0.2.1-alpha.34 → 0.2.1-alpha.36
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/config/environment.js +17 -0
- package/dist/aggregates.d.ts +21 -0
- package/dist/aggregates.js +90 -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 +174 -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 +47 -0
- package/dist/hooks.js +106 -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 +179 -0
- package/dist/manage-record.d.ts +13 -0
- package/dist/manage-record.js +114 -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 +245 -0
- package/dist/mysql/migration-runner.d.ts +12 -0
- package/dist/mysql/migration-runner.js +83 -0
- package/dist/mysql/mysql-db.d.ts +100 -0
- package/dist/mysql/mysql-db.js +415 -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 +286 -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 +455 -0
- package/dist/plural-registry.d.ts +4 -0
- package/{src → dist}/plural-registry.js +3 -6
- package/dist/postgres/connection.d.ts +15 -0
- package/dist/postgres/connection.js +30 -0
- package/dist/postgres/migration-generator.d.ts +45 -0
- package/dist/postgres/migration-generator.js +257 -0
- package/dist/postgres/migration-runner.d.ts +10 -0
- package/dist/postgres/migration-runner.js +82 -0
- package/dist/postgres/postgres-db.d.ts +119 -0
- package/dist/postgres/postgres-db.js +476 -0
- package/dist/postgres/query-builder.d.ts +27 -0
- package/dist/postgres/query-builder.js +98 -0
- package/dist/postgres/schema-introspector.d.ts +29 -0
- package/dist/postgres/schema-introspector.js +309 -0
- package/dist/postgres/type-map.d.ts +23 -0
- package/dist/postgres/type-map.js +53 -0
- package/dist/record.d.ts +75 -0
- package/dist/record.js +115 -0
- package/dist/relationships.d.ts +10 -0
- package/dist/relationships.js +39 -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 +54 -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 +271 -0
- package/dist/timescale/query-builder.d.ts +41 -0
- package/dist/timescale/query-builder.js +87 -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 +5 -0
- package/dist/utils.js +13 -0
- package/dist/view-resolver.d.ts +8 -0
- package/dist/view-resolver.js +169 -0
- package/dist/view.d.ts +11 -0
- package/dist/view.js +18 -0
- package/package.json +34 -11
- package/src/{aggregates.js → aggregates.ts} +27 -13
- package/src/{attr.js → attr.ts} +2 -2
- package/src/belongs-to.ts +90 -0
- package/src/{cli.js → cli.ts} +17 -11
- package/src/{commands.js → commands.ts} +179 -170
- package/src/{db.js → db.ts} +35 -26
- package/src/exports/db.ts +7 -0
- package/src/has-many.ts +92 -0
- package/src/{hooks.js → hooks.ts} +23 -27
- package/src/{index.js → index.ts} +4 -4
- package/src/{main.js → main.ts} +65 -34
- package/src/{manage-record.js → manage-record.ts} +42 -22
- package/src/{meta-request.js → meta-request.ts} +17 -14
- package/src/{migrate.js → migrate.ts} +9 -9
- package/src/{model-property.js → model-property.ts} +12 -6
- package/src/{model.js → model.ts} +5 -4
- package/src/mysql/{connection.js → connection.ts} +43 -28
- package/src/mysql/{migration-generator.js → migration-generator.ts} +332 -286
- package/src/mysql/{migration-runner.js → migration-runner.ts} +116 -110
- package/src/mysql/{mysql-db.js → mysql-db.ts} +537 -473
- package/src/mysql/{query-builder.js → query-builder.ts} +69 -64
- package/src/mysql/{schema-introspector.js → schema-introspector.ts} +355 -325
- package/src/mysql/{type-map.js → type-map.ts} +42 -37
- package/src/{orm-request.js → orm-request.ts} +169 -97
- package/src/plural-registry.ts +12 -0
- package/src/postgres/connection.ts +46 -0
- package/src/postgres/{migration-generator.js → migration-generator.ts} +82 -38
- package/src/postgres/{migration-runner.js → migration-runner.ts} +11 -10
- package/src/postgres/{postgres-db.js → postgres-db.ts} +202 -111
- package/src/postgres/{query-builder.js → query-builder.ts} +27 -28
- package/src/postgres/{schema-introspector.js → schema-introspector.ts} +87 -58
- package/src/postgres/{type-map.js → type-map.ts} +10 -6
- package/src/{record.js → record.ts} +73 -34
- package/src/relationships.ts +53 -0
- package/src/{serializer.js → serializer.ts} +52 -36
- package/src/{setup-rest-server.js → setup-rest-server.ts} +18 -13
- package/src/{standalone-db.js → standalone-db.ts} +33 -24
- package/src/{store.js → store.ts} +90 -68
- package/src/timescale/query-builder.ts +137 -0
- package/src/timescale/timescale-db.ts +119 -0
- package/src/transforms.ts +20 -0
- package/src/types/mysql2.d.ts +30 -0
- package/src/types/orm-types.ts +146 -0
- package/src/types/pg.d.ts +28 -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 +11 -0
- package/src/types/stonyx-utils.d.ts +33 -0
- package/src/types/stonyx.d.ts +21 -0
- package/src/utils.ts +16 -0
- package/src/{view-resolver.js → view-resolver.ts} +51 -24
- package/src/view.ts +22 -0
- package/src/belongs-to.js +0 -70
- package/src/has-many.js +0 -68
- package/src/postgres/connection.js +0 -30
- package/src/relationships.js +0 -43
- package/src/transforms.js +0 -20
- package/src/utils.js +0 -12
- package/src/view.js +0 -21
|
@@ -3,15 +3,70 @@ import { ensureMigrationsTable, getAppliedMigrations, getMigrationFiles, applyMi
|
|
|
3
3
|
import { introspectModels, introspectViews, getTopologicalOrder, schemasToSnapshot } from './schema-introspector.js';
|
|
4
4
|
import { loadLatestSnapshot, detectSchemaDrift } from './migration-generator.js';
|
|
5
5
|
import { buildInsert, buildUpdate, buildDelete, buildSelect, buildVectorSearch, buildHybridSearch } from './query-builder.js';
|
|
6
|
-
import {
|
|
6
|
+
import { store } from '@stonyx/orm';
|
|
7
|
+
import { createRecord } from '../manage-record.js';
|
|
7
8
|
import { confirm } from '@stonyx/utils/prompt';
|
|
8
9
|
import { readFile } from '@stonyx/utils/file';
|
|
9
10
|
import { getPluralName } from '../plural-registry.js';
|
|
11
|
+
import { isDbError } from '../utils.js';
|
|
10
12
|
import config from 'stonyx/config';
|
|
11
13
|
import log from 'stonyx/log';
|
|
12
14
|
import path from 'path';
|
|
15
|
+
import type { Pool } from 'pg';
|
|
16
|
+
import type { ForeignKeyDef, ModelSchema, OrmRecord } from '../types/orm-types.js';
|
|
13
17
|
|
|
14
|
-
|
|
18
|
+
interface PersistContext {
|
|
19
|
+
record?: OrmRecord;
|
|
20
|
+
recordId?: unknown;
|
|
21
|
+
oldState?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface PersistResponse {
|
|
25
|
+
data?: { id: unknown };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface SearchResult {
|
|
29
|
+
record: OrmRecord;
|
|
30
|
+
distance: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface VectorSearchOptions {
|
|
34
|
+
limit?: number;
|
|
35
|
+
where?: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface PostgresDeps {
|
|
39
|
+
getPool: typeof getPool;
|
|
40
|
+
closePool: typeof closePool;
|
|
41
|
+
ensureMigrationsTable: typeof ensureMigrationsTable;
|
|
42
|
+
getAppliedMigrations: typeof getAppliedMigrations;
|
|
43
|
+
getMigrationFiles: typeof getMigrationFiles;
|
|
44
|
+
applyMigration: typeof applyMigration;
|
|
45
|
+
parseMigrationFile: typeof parseMigrationFile;
|
|
46
|
+
introspectModels: typeof introspectModels;
|
|
47
|
+
introspectViews: typeof introspectViews;
|
|
48
|
+
getTopologicalOrder: typeof getTopologicalOrder;
|
|
49
|
+
schemasToSnapshot: typeof schemasToSnapshot;
|
|
50
|
+
loadLatestSnapshot: typeof loadLatestSnapshot;
|
|
51
|
+
detectSchemaDrift: typeof detectSchemaDrift;
|
|
52
|
+
buildInsert: typeof buildInsert;
|
|
53
|
+
buildUpdate: typeof buildUpdate;
|
|
54
|
+
buildDelete: typeof buildDelete;
|
|
55
|
+
buildSelect: typeof buildSelect;
|
|
56
|
+
buildVectorSearch: typeof buildVectorSearch;
|
|
57
|
+
buildHybridSearch: typeof buildHybridSearch;
|
|
58
|
+
createRecord: typeof createRecord;
|
|
59
|
+
store: typeof store;
|
|
60
|
+
confirm: typeof confirm;
|
|
61
|
+
readFile: typeof readFile;
|
|
62
|
+
getPluralName: typeof getPluralName;
|
|
63
|
+
config: typeof config;
|
|
64
|
+
log: typeof log;
|
|
65
|
+
path: typeof path;
|
|
66
|
+
[key: string]: unknown;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const defaultDeps: PostgresDeps = {
|
|
15
70
|
getPool, closePool, ensureMigrationsTable, getAppliedMigrations,
|
|
16
71
|
getMigrationFiles, applyMigration, parseMigrationFile,
|
|
17
72
|
introspectModels, introspectViews, getTopologicalOrder, schemasToSnapshot,
|
|
@@ -21,47 +76,69 @@ const defaultDeps = {
|
|
|
21
76
|
};
|
|
22
77
|
|
|
23
78
|
export default class PostgresDB {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
79
|
+
/** PostgreSQL extensions to enable on pool init. Subclasses can override. */
|
|
80
|
+
static extensions: string[] = ['vector'];
|
|
81
|
+
|
|
82
|
+
/** Config key under config.orm for this adapter. Subclasses can override. */
|
|
83
|
+
static configKey: string = 'postgres';
|
|
84
|
+
|
|
85
|
+
/** Singleton instance, set by subclass constructor name. */
|
|
86
|
+
static instance: PostgresDB | undefined;
|
|
87
|
+
|
|
88
|
+
deps!: PostgresDeps;
|
|
89
|
+
pool!: Pool | null;
|
|
90
|
+
pgConfig!: Record<string, unknown>;
|
|
91
|
+
|
|
92
|
+
constructor(deps: Partial<PostgresDeps> = {}) {
|
|
93
|
+
const Ctor = this.constructor as typeof PostgresDB;
|
|
94
|
+
if (Ctor.instance) return Ctor.instance;
|
|
95
|
+
Ctor.instance = this;
|
|
27
96
|
|
|
28
|
-
this.deps = { ...defaultDeps, ...deps };
|
|
97
|
+
this.deps = { ...defaultDeps, ...deps } as PostgresDeps;
|
|
29
98
|
this.pool = null;
|
|
30
|
-
this.pgConfig = this.deps.config.orm.
|
|
99
|
+
this.pgConfig = this.deps.config.orm[Ctor.configKey] as Record<string, unknown>;
|
|
31
100
|
}
|
|
32
101
|
|
|
33
|
-
|
|
34
|
-
this.pool
|
|
35
|
-
|
|
102
|
+
protected requirePool(): Pool {
|
|
103
|
+
if (!this.pool) throw new Error('PostgresDB pool not initialized — call init() first');
|
|
104
|
+
return this.pool;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async init(): Promise<void> {
|
|
108
|
+
this.pool = await this.deps.getPool(
|
|
109
|
+
this.pgConfig as unknown as Parameters<typeof getPool>[0],
|
|
110
|
+
(this.constructor as typeof PostgresDB).extensions
|
|
111
|
+
);
|
|
112
|
+
await this.deps.ensureMigrationsTable(this.pool, this.pgConfig.migrationsTable as string | undefined);
|
|
36
113
|
await this.loadMemoryRecords();
|
|
37
114
|
}
|
|
38
115
|
|
|
39
|
-
async startup() {
|
|
40
|
-
const migrationsPath = this.deps.path.resolve(this.deps.config.rootPath, this.pgConfig.migrationsDir);
|
|
116
|
+
async startup(): Promise<void> {
|
|
117
|
+
const migrationsPath = this.deps.path.resolve(this.deps.config.rootPath, this.pgConfig.migrationsDir as string);
|
|
41
118
|
|
|
42
119
|
// Check for pending migrations
|
|
43
|
-
const applied = await this.deps.getAppliedMigrations(this.
|
|
120
|
+
const applied = await this.deps.getAppliedMigrations(this.requirePool(), this.pgConfig.migrationsTable as string | undefined);
|
|
44
121
|
const files = await this.deps.getMigrationFiles(migrationsPath);
|
|
45
122
|
const pending = files.filter(f => !applied.includes(f));
|
|
46
123
|
|
|
47
124
|
if (pending.length > 0) {
|
|
48
|
-
this.deps.log.db(`${pending.length} pending migration(s) found.`);
|
|
125
|
+
this.deps.log.db!(`${pending.length} pending migration(s) found.`);
|
|
49
126
|
|
|
50
127
|
const shouldApply = await this.deps.confirm(`${pending.length} pending migration(s) found. Apply now?`);
|
|
51
128
|
|
|
52
129
|
if (shouldApply) {
|
|
53
130
|
for (const filename of pending) {
|
|
54
|
-
const content = await this.deps.readFile(this.deps.path.join(migrationsPath, filename));
|
|
131
|
+
const content = await this.deps.readFile(this.deps.path.join(migrationsPath, filename)) as string;
|
|
55
132
|
const { up } = this.deps.parseMigrationFile(content);
|
|
56
133
|
|
|
57
|
-
await this.deps.applyMigration(this.
|
|
58
|
-
this.deps.log.db(`Applied migration: ${filename}`);
|
|
134
|
+
await this.deps.applyMigration(this.requirePool(), filename, up, this.pgConfig.migrationsTable as string | undefined);
|
|
135
|
+
this.deps.log.db!(`Applied migration: ${filename}`);
|
|
59
136
|
}
|
|
60
137
|
|
|
61
138
|
// Reload records after applying migrations
|
|
62
139
|
await this.loadMemoryRecords();
|
|
63
140
|
} else {
|
|
64
|
-
this.deps.log.warn('Skipping pending migrations. Schema may be outdated.');
|
|
141
|
+
this.deps.log.warn!('Skipping pending migrations. Schema may be outdated.');
|
|
65
142
|
}
|
|
66
143
|
} else if (files.length === 0) {
|
|
67
144
|
const schemas = this.deps.introspectModels();
|
|
@@ -78,52 +155,52 @@ export default class PostgresDB {
|
|
|
78
155
|
|
|
79
156
|
if (result) {
|
|
80
157
|
const { up } = this.deps.parseMigrationFile(result.content);
|
|
81
|
-
await this.deps.applyMigration(this.
|
|
82
|
-
this.deps.log.db(`Applied migration: ${result.filename}`);
|
|
158
|
+
await this.deps.applyMigration(this.requirePool(), result.filename, up, this.pgConfig.migrationsTable as string | undefined);
|
|
159
|
+
this.deps.log.db!(`Applied migration: ${result.filename}`);
|
|
83
160
|
await this.loadMemoryRecords();
|
|
84
161
|
}
|
|
85
162
|
} else {
|
|
86
|
-
this.deps.log.warn('Skipping initial migration. Tables may not exist.');
|
|
163
|
+
this.deps.log.warn!('Skipping initial migration. Tables may not exist.');
|
|
87
164
|
}
|
|
88
165
|
}
|
|
89
166
|
}
|
|
90
167
|
|
|
91
168
|
// Check for schema drift
|
|
92
169
|
const schemas = this.deps.introspectModels();
|
|
93
|
-
const snapshot = await this.deps.loadLatestSnapshot(this.deps.path.resolve(this.deps.config.rootPath, this.pgConfig.migrationsDir));
|
|
170
|
+
const snapshot = await this.deps.loadLatestSnapshot(this.deps.path.resolve(this.deps.config.rootPath, this.pgConfig.migrationsDir as string));
|
|
94
171
|
|
|
95
172
|
if (Object.keys(snapshot).length > 0) {
|
|
96
|
-
const drift = this.deps.detectSchemaDrift(schemas, snapshot);
|
|
173
|
+
const drift = this.deps.detectSchemaDrift(schemas, snapshot as Parameters<typeof detectSchemaDrift>[1]);
|
|
97
174
|
|
|
98
175
|
if (drift.hasChanges) {
|
|
99
|
-
this.deps.log.warn('Schema drift detected: models have changed since the last migration.');
|
|
100
|
-
this.deps.log.warn('Run `stonyx db:generate-migration` to create a new migration.');
|
|
176
|
+
this.deps.log.warn!('Schema drift detected: models have changed since the last migration.');
|
|
177
|
+
this.deps.log.warn!('Run `stonyx db:generate-migration` to create a new migration.');
|
|
101
178
|
}
|
|
102
179
|
}
|
|
103
180
|
}
|
|
104
181
|
|
|
105
|
-
async shutdown() {
|
|
182
|
+
async shutdown(): Promise<void> {
|
|
106
183
|
await this.deps.closePool();
|
|
107
184
|
this.pool = null;
|
|
108
185
|
}
|
|
109
186
|
|
|
110
|
-
async save() {
|
|
187
|
+
async save(): Promise<void> {
|
|
111
188
|
// No-op: PostgreSQL persists data immediately via persist()
|
|
112
189
|
}
|
|
113
190
|
|
|
114
191
|
/**
|
|
115
192
|
* Loads only models with memory: true into the in-memory store on startup.
|
|
116
|
-
* Models with memory: false are skipped
|
|
193
|
+
* Models with memory: false are skipped -- accessed on-demand via find()/findAll().
|
|
117
194
|
*/
|
|
118
|
-
async loadMemoryRecords() {
|
|
195
|
+
async loadMemoryRecords(): Promise<void> {
|
|
119
196
|
const schemas = this.deps.introspectModels();
|
|
120
197
|
const order = this.deps.getTopologicalOrder(schemas);
|
|
121
198
|
const Orm = (await import('@stonyx/orm')).default;
|
|
122
199
|
|
|
123
200
|
for (const modelName of order) {
|
|
124
|
-
const { modelClass } = Orm.instance.getRecordClasses(modelName);
|
|
201
|
+
const { modelClass } = Orm.instance.getRecordClasses(modelName) as { modelClass: { memory?: boolean } };
|
|
125
202
|
if (modelClass?.memory === false) {
|
|
126
|
-
this.deps.log.db(`Skipping memory load for '${modelName}' (memory: false)`);
|
|
203
|
+
this.deps.log.db!(`Skipping memory load for '${modelName}' (memory: false)`);
|
|
127
204
|
continue;
|
|
128
205
|
}
|
|
129
206
|
|
|
@@ -131,7 +208,7 @@ export default class PostgresDB {
|
|
|
131
208
|
const { sql, values } = this.deps.buildSelect(schema.table);
|
|
132
209
|
|
|
133
210
|
try {
|
|
134
|
-
const result = await this.
|
|
211
|
+
const result = await this.requirePool().query(sql, values);
|
|
135
212
|
|
|
136
213
|
for (const row of result.rows) {
|
|
137
214
|
const rawData = this._rowToRawData(row, schema);
|
|
@@ -139,8 +216,8 @@ export default class PostgresDB {
|
|
|
139
216
|
}
|
|
140
217
|
} catch (error) {
|
|
141
218
|
// 42P01 = undefined_table (PG equivalent of ER_NO_SUCH_TABLE)
|
|
142
|
-
if (error.code === '42P01') {
|
|
143
|
-
this.deps.log.db(`Table '${schema.table}' does not exist yet. Skipping load for '${modelName}'.`);
|
|
219
|
+
if (isDbError(error) && error.code === '42P01') {
|
|
220
|
+
this.deps.log.db!(`Table '${schema.table}' does not exist yet. Skipping load for '${modelName}'.`);
|
|
144
221
|
continue;
|
|
145
222
|
}
|
|
146
223
|
|
|
@@ -152,25 +229,34 @@ export default class PostgresDB {
|
|
|
152
229
|
const viewSchemas = this.deps.introspectViews();
|
|
153
230
|
|
|
154
231
|
for (const [viewName, viewSchema] of Object.entries(viewSchemas)) {
|
|
155
|
-
const { modelClass: viewClass } = Orm.instance.getRecordClasses(viewName);
|
|
232
|
+
const { modelClass: viewClass } = Orm.instance.getRecordClasses(viewName) as { modelClass: { memory?: boolean } };
|
|
156
233
|
if (viewClass?.memory !== true) {
|
|
157
|
-
this.deps.log.db(`Skipping memory load for view '${viewName}' (memory: false)`);
|
|
234
|
+
this.deps.log.db!(`Skipping memory load for view '${viewName}' (memory: false)`);
|
|
158
235
|
continue;
|
|
159
236
|
}
|
|
160
237
|
|
|
161
|
-
const
|
|
238
|
+
const sourceIdType = schemas[viewSchema.source]?.idType || 'number';
|
|
239
|
+
const schema: ModelSchema = {
|
|
240
|
+
table: viewSchema.viewName,
|
|
241
|
+
idType: sourceIdType,
|
|
242
|
+
columns: viewSchema.columns || {},
|
|
243
|
+
foreignKeys: (viewSchema.foreignKeys || {}) as Record<string, ForeignKeyDef>,
|
|
244
|
+
relationships: { belongsTo: {}, hasMany: {} },
|
|
245
|
+
vectorColumns: {},
|
|
246
|
+
memory: false,
|
|
247
|
+
};
|
|
162
248
|
const { sql, values } = this.deps.buildSelect(schema.table);
|
|
163
249
|
|
|
164
250
|
try {
|
|
165
|
-
const result = await this.
|
|
251
|
+
const result = await this.requirePool().query(sql, values);
|
|
166
252
|
|
|
167
253
|
for (const row of result.rows) {
|
|
168
254
|
const rawData = this._rowToRawData(row, schema);
|
|
169
255
|
this.deps.createRecord(viewName, rawData, { isDbRecord: true, serialize: false, transform: false });
|
|
170
256
|
}
|
|
171
257
|
} catch (error) {
|
|
172
|
-
if (error.code === '42P01') {
|
|
173
|
-
this.deps.log.db(`View '${viewSchema.viewName}' does not exist yet. Skipping load for '${viewName}'.`);
|
|
258
|
+
if (isDbError(error) && error.code === '42P01') {
|
|
259
|
+
this.deps.log.db!(`View '${viewSchema.viewName}' does not exist yet. Skipping load for '${viewName}'.`);
|
|
174
260
|
continue;
|
|
175
261
|
}
|
|
176
262
|
throw error;
|
|
@@ -181,27 +267,33 @@ export default class PostgresDB {
|
|
|
181
267
|
/**
|
|
182
268
|
* @deprecated Use loadMemoryRecords() instead. Kept for backward compatibility.
|
|
183
269
|
*/
|
|
184
|
-
async loadAllRecords() {
|
|
270
|
+
async loadAllRecords(): Promise<void> {
|
|
185
271
|
return this.loadMemoryRecords();
|
|
186
272
|
}
|
|
187
273
|
|
|
188
274
|
/**
|
|
189
275
|
* Find a single record by ID from PostgreSQL.
|
|
190
276
|
* Does NOT cache the result in the store for memory: false models.
|
|
191
|
-
* @param {string} modelName
|
|
192
|
-
* @param {string|number} id
|
|
193
|
-
* @returns {Promise<Record|undefined>}
|
|
194
277
|
*/
|
|
195
|
-
async findRecord(modelName, id) {
|
|
278
|
+
async findRecord(modelName: string, id: string | number): Promise<OrmRecord | undefined> {
|
|
196
279
|
const schemas = this.deps.introspectModels();
|
|
197
|
-
let schema = schemas[modelName];
|
|
280
|
+
let schema: ModelSchema | undefined = schemas[modelName];
|
|
198
281
|
|
|
199
282
|
// Check views if not found in models
|
|
200
283
|
if (!schema) {
|
|
201
284
|
const viewSchemas = this.deps.introspectViews();
|
|
202
285
|
const viewSchema = viewSchemas[modelName];
|
|
203
286
|
if (viewSchema) {
|
|
204
|
-
|
|
287
|
+
const sourceIdType = schemas[viewSchema.source]?.idType || 'number';
|
|
288
|
+
schema = {
|
|
289
|
+
table: viewSchema.viewName,
|
|
290
|
+
idType: sourceIdType,
|
|
291
|
+
columns: viewSchema.columns || {},
|
|
292
|
+
foreignKeys: (viewSchema.foreignKeys || {}) as Record<string, ForeignKeyDef>,
|
|
293
|
+
relationships: { belongsTo: {}, hasMany: {} },
|
|
294
|
+
vectorColumns: {},
|
|
295
|
+
memory: false,
|
|
296
|
+
};
|
|
205
297
|
}
|
|
206
298
|
}
|
|
207
299
|
|
|
@@ -210,38 +302,44 @@ export default class PostgresDB {
|
|
|
210
302
|
const { sql, values } = this.deps.buildSelect(schema.table, { id });
|
|
211
303
|
|
|
212
304
|
try {
|
|
213
|
-
const result = await this.
|
|
305
|
+
const result = await this.requirePool().query(sql, values);
|
|
214
306
|
|
|
215
307
|
if (result.rows.length === 0) return undefined;
|
|
216
308
|
|
|
217
309
|
const rawData = this._rowToRawData(result.rows[0], schema);
|
|
218
|
-
const record = this.deps.createRecord(modelName, rawData, { isDbRecord: true, serialize: false, transform: false });
|
|
310
|
+
const record = this.deps.createRecord(modelName, rawData, { isDbRecord: true, serialize: false, transform: false }) as unknown as OrmRecord;
|
|
219
311
|
|
|
220
312
|
this._evictIfNotMemory(modelName, record);
|
|
221
313
|
|
|
222
314
|
return record;
|
|
223
315
|
} catch (error) {
|
|
224
|
-
if (error.code === '42P01') return undefined;
|
|
316
|
+
if (isDbError(error) && error.code === '42P01') return undefined;
|
|
225
317
|
throw error;
|
|
226
318
|
}
|
|
227
319
|
}
|
|
228
320
|
|
|
229
321
|
/**
|
|
230
322
|
* Find all records of a model from PostgreSQL, with optional conditions.
|
|
231
|
-
* @param {string} modelName
|
|
232
|
-
* @param {Object} [conditions] - Optional WHERE conditions (key-value pairs)
|
|
233
|
-
* @returns {Promise<Record[]>}
|
|
234
323
|
*/
|
|
235
|
-
async findAll(modelName, conditions) {
|
|
324
|
+
async findAll(modelName: string, conditions?: Record<string, unknown>): Promise<OrmRecord[]> {
|
|
236
325
|
const schemas = this.deps.introspectModels();
|
|
237
|
-
let schema = schemas[modelName];
|
|
326
|
+
let schema: ModelSchema | undefined = schemas[modelName];
|
|
238
327
|
|
|
239
328
|
// Check views if not found in models
|
|
240
329
|
if (!schema) {
|
|
241
330
|
const viewSchemas = this.deps.introspectViews();
|
|
242
331
|
const viewSchema = viewSchemas[modelName];
|
|
243
332
|
if (viewSchema) {
|
|
244
|
-
|
|
333
|
+
const sourceIdType = schemas[viewSchema.source]?.idType || 'number';
|
|
334
|
+
schema = {
|
|
335
|
+
table: viewSchema.viewName,
|
|
336
|
+
idType: sourceIdType,
|
|
337
|
+
columns: viewSchema.columns || {},
|
|
338
|
+
foreignKeys: (viewSchema.foreignKeys || {}) as Record<string, ForeignKeyDef>,
|
|
339
|
+
relationships: { belongsTo: {}, hasMany: {} },
|
|
340
|
+
vectorColumns: {},
|
|
341
|
+
memory: false,
|
|
342
|
+
};
|
|
245
343
|
}
|
|
246
344
|
}
|
|
247
345
|
|
|
@@ -250,11 +348,11 @@ export default class PostgresDB {
|
|
|
250
348
|
const { sql, values } = this.deps.buildSelect(schema.table, conditions);
|
|
251
349
|
|
|
252
350
|
try {
|
|
253
|
-
const result = await this.
|
|
351
|
+
const result = await this.requirePool().query(sql, values);
|
|
254
352
|
|
|
255
353
|
const records = result.rows.map(row => {
|
|
256
|
-
const rawData = this._rowToRawData(row, schema);
|
|
257
|
-
return this.deps.createRecord(modelName, rawData, { isDbRecord: true, serialize: false, transform: false });
|
|
354
|
+
const rawData = this._rowToRawData(row, schema!);
|
|
355
|
+
return this.deps.createRecord(modelName, rawData, { isDbRecord: true, serialize: false, transform: false }) as unknown as OrmRecord;
|
|
258
356
|
});
|
|
259
357
|
|
|
260
358
|
for (const record of records) {
|
|
@@ -263,22 +361,15 @@ export default class PostgresDB {
|
|
|
263
361
|
|
|
264
362
|
return records;
|
|
265
363
|
} catch (error) {
|
|
266
|
-
if (error.code === '42P01') return [];
|
|
364
|
+
if (isDbError(error) && error.code === '42P01') return [];
|
|
267
365
|
throw error;
|
|
268
366
|
}
|
|
269
367
|
}
|
|
270
368
|
|
|
271
369
|
/**
|
|
272
370
|
* Perform a vector similarity search using cosine distance.
|
|
273
|
-
* @param {string} modelName
|
|
274
|
-
* @param {string} vectorColumn - Name of the vector column
|
|
275
|
-
* @param {number[]} queryVector - The query vector
|
|
276
|
-
* @param {Object} [options]
|
|
277
|
-
* @param {number} [options.limit=10]
|
|
278
|
-
* @param {Object} [options.where] - Additional conditions
|
|
279
|
-
* @returns {Promise<{ record: Record, distance: number }[]>}
|
|
280
371
|
*/
|
|
281
|
-
async vectorSearch(modelName, vectorColumn, queryVector, options = {}) {
|
|
372
|
+
async vectorSearch(modelName: string, vectorColumn: string, queryVector: number[], options: VectorSearchOptions = {}): Promise<SearchResult[]> {
|
|
282
373
|
const schemas = this.deps.introspectModels();
|
|
283
374
|
const schema = schemas[modelName];
|
|
284
375
|
if (!schema) return [];
|
|
@@ -286,33 +377,26 @@ export default class PostgresDB {
|
|
|
286
377
|
const { sql, values } = this.deps.buildVectorSearch(schema.table, vectorColumn, queryVector, options);
|
|
287
378
|
|
|
288
379
|
try {
|
|
289
|
-
const result = await this.
|
|
380
|
+
const result = await this.requirePool().query(sql, values);
|
|
290
381
|
|
|
291
382
|
return result.rows.map(row => {
|
|
292
|
-
const distance = row.distance;
|
|
383
|
+
const distance = row.distance as number;
|
|
293
384
|
delete row.distance;
|
|
294
385
|
const rawData = this._rowToRawData(row, schema);
|
|
295
|
-
const record = this.deps.createRecord(modelName, rawData, { isDbRecord: true, serialize: false, transform: false });
|
|
386
|
+
const record = this.deps.createRecord(modelName, rawData, { isDbRecord: true, serialize: false, transform: false }) as unknown as OrmRecord;
|
|
296
387
|
this._evictIfNotMemory(modelName, record);
|
|
297
388
|
return { record, distance };
|
|
298
389
|
});
|
|
299
390
|
} catch (error) {
|
|
300
|
-
if (error.code === '42P01') return [];
|
|
391
|
+
if (isDbError(error) && error.code === '42P01') return [];
|
|
301
392
|
throw error;
|
|
302
393
|
}
|
|
303
394
|
}
|
|
304
395
|
|
|
305
396
|
/**
|
|
306
397
|
* Perform a hybrid search combining vector similarity with text filtering.
|
|
307
|
-
* @param {string} modelName
|
|
308
|
-
* @param {string} vectorColumn
|
|
309
|
-
* @param {number[]} queryVector
|
|
310
|
-
* @param {string} textColumn
|
|
311
|
-
* @param {string} textQuery
|
|
312
|
-
* @param {Object} [options]
|
|
313
|
-
* @returns {Promise<{ record: Record, distance: number }[]>}
|
|
314
398
|
*/
|
|
315
|
-
async hybridSearch(modelName, vectorColumn, queryVector, textColumn, textQuery, options = {}) {
|
|
399
|
+
async hybridSearch(modelName: string, vectorColumn: string, queryVector: number[], textColumn: string, textQuery: string, options: VectorSearchOptions = {}): Promise<SearchResult[]> {
|
|
316
400
|
const schemas = this.deps.introspectModels();
|
|
317
401
|
const schema = schemas[modelName];
|
|
318
402
|
if (!schema) return [];
|
|
@@ -320,40 +404,44 @@ export default class PostgresDB {
|
|
|
320
404
|
const { sql, values } = this.deps.buildHybridSearch(schema.table, vectorColumn, queryVector, textColumn, textQuery, options);
|
|
321
405
|
|
|
322
406
|
try {
|
|
323
|
-
const result = await this.
|
|
407
|
+
const result = await this.requirePool().query(sql, values);
|
|
324
408
|
|
|
325
409
|
return result.rows.map(row => {
|
|
326
|
-
const distance = row.distance;
|
|
410
|
+
const distance = row.distance as number;
|
|
327
411
|
delete row.distance;
|
|
328
412
|
const rawData = this._rowToRawData(row, schema);
|
|
329
|
-
const record = this.deps.createRecord(modelName, rawData, { isDbRecord: true, serialize: false, transform: false });
|
|
413
|
+
const record = this.deps.createRecord(modelName, rawData, { isDbRecord: true, serialize: false, transform: false }) as unknown as OrmRecord;
|
|
330
414
|
this._evictIfNotMemory(modelName, record);
|
|
331
415
|
return { record, distance };
|
|
332
416
|
});
|
|
333
417
|
} catch (error) {
|
|
334
|
-
if (error.code === '42P01') return [];
|
|
418
|
+
if (isDbError(error) && error.code === '42P01') return [];
|
|
335
419
|
throw error;
|
|
336
420
|
}
|
|
337
421
|
}
|
|
338
422
|
|
|
339
423
|
/**
|
|
340
424
|
* Remove a record from the in-memory store if its model has memory: false.
|
|
341
|
-
* The record object itself survives
|
|
425
|
+
* The record object itself survives -- the caller retains the reference.
|
|
342
426
|
* @private
|
|
343
427
|
*/
|
|
344
|
-
_evictIfNotMemory(modelName, record) {
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
428
|
+
private _evictIfNotMemory(modelName: string, record: OrmRecord): void {
|
|
429
|
+
const storeRef = this.deps.store as {
|
|
430
|
+
_memoryResolver?: (name: string) => boolean;
|
|
431
|
+
get?: (name: string) => Map<unknown, unknown> | undefined;
|
|
432
|
+
data?: { get(name: string): Map<unknown, unknown> | undefined };
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
if (storeRef._memoryResolver && !storeRef._memoryResolver(modelName)) {
|
|
436
|
+
const modelStore = storeRef.get?.(modelName) ?? storeRef.data?.get(modelName);
|
|
349
437
|
if (modelStore) modelStore.delete(record.id);
|
|
350
438
|
}
|
|
351
439
|
}
|
|
352
440
|
|
|
353
|
-
_rowToRawData(row, schema) {
|
|
354
|
-
const rawData = { ...row };
|
|
441
|
+
private _rowToRawData(row: Record<string, unknown>, schema: ModelSchema): Record<string, unknown> {
|
|
442
|
+
const rawData: Record<string, unknown> = { ...row };
|
|
355
443
|
|
|
356
|
-
// PostgreSQL returns native booleans and parsed JSONB
|
|
444
|
+
// PostgreSQL returns native booleans and parsed JSONB -- no manual conversion needed.
|
|
357
445
|
// Only FK remapping and timestamp stripping required.
|
|
358
446
|
|
|
359
447
|
// Map FK columns back to relationship keys
|
|
@@ -366,17 +454,17 @@ export default class PostgresDB {
|
|
|
366
454
|
}
|
|
367
455
|
}
|
|
368
456
|
|
|
369
|
-
// Remove timestamp columns
|
|
457
|
+
// Remove timestamp columns -- managed by PostgreSQL
|
|
370
458
|
delete rawData.created_at;
|
|
371
459
|
delete rawData.updated_at;
|
|
372
460
|
|
|
373
461
|
return rawData;
|
|
374
462
|
}
|
|
375
463
|
|
|
376
|
-
async persist(operation, modelName, context, response) {
|
|
377
|
-
// Views are read-only
|
|
464
|
+
async persist(operation: string, modelName: string, context: PersistContext, response: PersistResponse): Promise<void> {
|
|
465
|
+
// Views are read-only -- no-op for all write operations
|
|
378
466
|
const Orm = (await import('@stonyx/orm')).default;
|
|
379
|
-
if (Orm.instance?.isView?.(modelName)) return;
|
|
467
|
+
if ((Orm.instance as { isView?: (name: string) => boolean })?.isView?.(modelName)) return;
|
|
380
468
|
|
|
381
469
|
switch (operation) {
|
|
382
470
|
case 'create':
|
|
@@ -388,14 +476,17 @@ export default class PostgresDB {
|
|
|
388
476
|
}
|
|
389
477
|
}
|
|
390
478
|
|
|
391
|
-
async _persistCreate(modelName,
|
|
479
|
+
private async _persistCreate(modelName: string, _context: PersistContext, response: PersistResponse): Promise<void> {
|
|
392
480
|
const schemas = this.deps.introspectModels();
|
|
393
481
|
const schema = schemas[modelName];
|
|
394
482
|
|
|
395
483
|
if (!schema) return;
|
|
396
484
|
|
|
397
485
|
const recordId = response?.data?.id;
|
|
398
|
-
const
|
|
486
|
+
const storeRef = this.deps.store as unknown as {
|
|
487
|
+
get(name: string, id: unknown): OrmRecord | null;
|
|
488
|
+
};
|
|
489
|
+
const record = recordId != null ? storeRef.get(modelName, isNaN(recordId as number) ? recordId : parseInt(recordId as string)) : null;
|
|
399
490
|
|
|
400
491
|
if (!record) return;
|
|
401
492
|
|
|
@@ -410,13 +501,13 @@ export default class PostgresDB {
|
|
|
410
501
|
|
|
411
502
|
const { sql, values } = this.deps.buildInsert(schema.table, insertData);
|
|
412
503
|
|
|
413
|
-
const result = await this.
|
|
504
|
+
const result = await this.requirePool().query(sql, values);
|
|
414
505
|
|
|
415
506
|
// Re-key the record in the store if PostgreSQL generated the ID (via RETURNING)
|
|
416
507
|
if (isPendingId && result.rows.length > 0) {
|
|
417
508
|
const pendingId = record.id;
|
|
418
509
|
const realId = result.rows[0].id;
|
|
419
|
-
const modelStore = this.deps.store.get(modelName);
|
|
510
|
+
const modelStore = (this.deps.store as unknown as { get(name: string): Map<unknown, unknown> }).get(modelName);
|
|
420
511
|
|
|
421
512
|
modelStore.delete(pendingId);
|
|
422
513
|
record.__data.id = realId;
|
|
@@ -432,7 +523,7 @@ export default class PostgresDB {
|
|
|
432
523
|
}
|
|
433
524
|
}
|
|
434
525
|
|
|
435
|
-
async _persistUpdate(modelName, context,
|
|
526
|
+
private async _persistUpdate(modelName: string, context: PersistContext, _response: PersistResponse): Promise<void> {
|
|
436
527
|
const schemas = this.deps.introspectModels();
|
|
437
528
|
const schema = schemas[modelName];
|
|
438
529
|
|
|
@@ -446,7 +537,7 @@ export default class PostgresDB {
|
|
|
446
537
|
const currentData = record.__data;
|
|
447
538
|
|
|
448
539
|
// Build a diff of changed columns
|
|
449
|
-
const changedData = {};
|
|
540
|
+
const changedData: Record<string, unknown> = {};
|
|
450
541
|
|
|
451
542
|
for (const [col] of Object.entries(schema.columns)) {
|
|
452
543
|
if (currentData[col] !== oldState[col]) {
|
|
@@ -457,7 +548,7 @@ export default class PostgresDB {
|
|
|
457
548
|
// Check FK changes too
|
|
458
549
|
for (const fkCol of Object.keys(schema.foreignKeys)) {
|
|
459
550
|
const relName = fkCol.replace(/_id$/, '');
|
|
460
|
-
const currentFkValue = record.__relationships[relName]?.id ?? null;
|
|
551
|
+
const currentFkValue = (record.__relationships[relName] as { id: unknown } | undefined)?.id ?? null;
|
|
461
552
|
const oldFkValue = oldState[relName] ?? null;
|
|
462
553
|
|
|
463
554
|
if (currentFkValue !== oldFkValue) {
|
|
@@ -467,14 +558,14 @@ export default class PostgresDB {
|
|
|
467
558
|
|
|
468
559
|
if (Object.keys(changedData).length === 0) return;
|
|
469
560
|
|
|
470
|
-
// PostgreSQL doesn't have ON UPDATE CURRENT_TIMESTAMP
|
|
561
|
+
// PostgreSQL doesn't have ON UPDATE CURRENT_TIMESTAMP -- set updated_at manually
|
|
471
562
|
changedData.updated_at = new Date();
|
|
472
563
|
|
|
473
564
|
const { sql, values } = this.deps.buildUpdate(schema.table, id, changedData);
|
|
474
|
-
await this.
|
|
565
|
+
await this.requirePool().query(sql, values);
|
|
475
566
|
}
|
|
476
567
|
|
|
477
|
-
async _persistDelete(modelName, context) {
|
|
568
|
+
private async _persistDelete(modelName: string, context: PersistContext): Promise<void> {
|
|
478
569
|
const schemas = this.deps.introspectModels();
|
|
479
570
|
const schema = schemas[modelName];
|
|
480
571
|
|
|
@@ -484,11 +575,11 @@ export default class PostgresDB {
|
|
|
484
575
|
if (id == null) return;
|
|
485
576
|
|
|
486
577
|
const { sql, values } = this.deps.buildDelete(schema.table, id);
|
|
487
|
-
await this.
|
|
578
|
+
await this.requirePool().query(sql, values);
|
|
488
579
|
}
|
|
489
580
|
|
|
490
|
-
_recordToRow(record, schema) {
|
|
491
|
-
const row = {};
|
|
581
|
+
private _recordToRow(record: OrmRecord, schema: ModelSchema): Record<string, unknown> {
|
|
582
|
+
const row: Record<string, unknown> = {};
|
|
492
583
|
const data = record.__data;
|
|
493
584
|
|
|
494
585
|
// ID
|
|
@@ -512,7 +603,7 @@ export default class PostgresDB {
|
|
|
512
603
|
const related = record.__relationships[relName];
|
|
513
604
|
|
|
514
605
|
if (related) {
|
|
515
|
-
row[fkCol] = related.id;
|
|
606
|
+
row[fkCol] = (related as { id: unknown }).id;
|
|
516
607
|
} else if (data[relName] !== undefined) {
|
|
517
608
|
// Raw FK value (e.g., from create payload)
|
|
518
609
|
row[fkCol] = data[relName];
|