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