@mostajs/orm 1.0.0
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/LICENSE +21 -0
- package/README.md +548 -0
- package/dist/core/base-repository.d.ts +26 -0
- package/dist/core/base-repository.js +82 -0
- package/dist/core/config.d.ts +62 -0
- package/dist/core/config.js +116 -0
- package/dist/core/errors.d.ts +30 -0
- package/dist/core/errors.js +49 -0
- package/dist/core/factory.d.ts +41 -0
- package/dist/core/factory.js +142 -0
- package/dist/core/normalizer.d.ts +9 -0
- package/dist/core/normalizer.js +19 -0
- package/dist/core/registry.d.ts +43 -0
- package/dist/core/registry.js +78 -0
- package/dist/core/types.d.ts +228 -0
- package/dist/core/types.js +5 -0
- package/dist/dialects/abstract-sql.dialect.d.ts +113 -0
- package/dist/dialects/abstract-sql.dialect.js +1071 -0
- package/dist/dialects/cockroachdb.dialect.d.ts +2 -0
- package/dist/dialects/cockroachdb.dialect.js +23 -0
- package/dist/dialects/db2.dialect.d.ts +2 -0
- package/dist/dialects/db2.dialect.js +190 -0
- package/dist/dialects/hana.dialect.d.ts +2 -0
- package/dist/dialects/hana.dialect.js +199 -0
- package/dist/dialects/hsqldb.dialect.d.ts +2 -0
- package/dist/dialects/hsqldb.dialect.js +114 -0
- package/dist/dialects/mariadb.dialect.d.ts +2 -0
- package/dist/dialects/mariadb.dialect.js +87 -0
- package/dist/dialects/mongo.dialect.d.ts +2 -0
- package/dist/dialects/mongo.dialect.js +480 -0
- package/dist/dialects/mssql.dialect.d.ts +27 -0
- package/dist/dialects/mssql.dialect.js +127 -0
- package/dist/dialects/mysql.dialect.d.ts +24 -0
- package/dist/dialects/mysql.dialect.js +101 -0
- package/dist/dialects/oracle.dialect.d.ts +2 -0
- package/dist/dialects/oracle.dialect.js +206 -0
- package/dist/dialects/postgres.dialect.d.ts +26 -0
- package/dist/dialects/postgres.dialect.js +105 -0
- package/dist/dialects/spanner.dialect.d.ts +2 -0
- package/dist/dialects/spanner.dialect.js +259 -0
- package/dist/dialects/sqlite.dialect.d.ts +2 -0
- package/dist/dialects/sqlite.dialect.js +1027 -0
- package/dist/dialects/sybase.dialect.d.ts +2 -0
- package/dist/dialects/sybase.dialect.js +119 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +26 -0
- package/docs/api-reference.md +1009 -0
- package/docs/dialects.md +673 -0
- package/docs/tutorial.md +846 -0
- package/package.json +91 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// Google Cloud Spanner Dialect — extends AbstractSqlDialect
|
|
2
|
+
// Equivalent to org.hibernate.dialect.SpannerDialect (Hibernate ORM 6.4)
|
|
3
|
+
// Driver: npm install @google-cloud/spanner
|
|
4
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
5
|
+
import { AbstractSqlDialect } from './abstract-sql.dialect.js';
|
|
6
|
+
// ============================================================
|
|
7
|
+
// Type Mapping — DAL FieldType → Spanner column type
|
|
8
|
+
// ============================================================
|
|
9
|
+
const SPANNER_TYPE_MAP = {
|
|
10
|
+
string: 'STRING(MAX)',
|
|
11
|
+
number: 'FLOAT64',
|
|
12
|
+
boolean: 'BOOL',
|
|
13
|
+
date: 'TIMESTAMP',
|
|
14
|
+
json: 'JSON',
|
|
15
|
+
array: 'JSON',
|
|
16
|
+
};
|
|
17
|
+
// ============================================================
|
|
18
|
+
// SpannerDialect
|
|
19
|
+
// ============================================================
|
|
20
|
+
class SpannerDialect extends AbstractSqlDialect {
|
|
21
|
+
dialectType = 'spanner';
|
|
22
|
+
instance = null;
|
|
23
|
+
database = null;
|
|
24
|
+
spannerClient = null;
|
|
25
|
+
// --- Abstract implementations ---
|
|
26
|
+
// Spanner uses backtick quoting
|
|
27
|
+
quoteIdentifier(name) {
|
|
28
|
+
return `\`${name}\``;
|
|
29
|
+
}
|
|
30
|
+
// Spanner uses @p1, @p2, ... for named parameters
|
|
31
|
+
getPlaceholder(index) {
|
|
32
|
+
return `@p${index}`;
|
|
33
|
+
}
|
|
34
|
+
fieldToSqlType(field) {
|
|
35
|
+
return SPANNER_TYPE_MAP[field.type] || 'STRING(MAX)';
|
|
36
|
+
}
|
|
37
|
+
getIdColumnType() {
|
|
38
|
+
return 'STRING(36)';
|
|
39
|
+
}
|
|
40
|
+
getTableListQuery() {
|
|
41
|
+
return "SELECT table_name as name FROM information_schema.tables WHERE table_schema = ''";
|
|
42
|
+
}
|
|
43
|
+
// --- Hooks ---
|
|
44
|
+
// Spanner doesn't support IF NOT EXISTS
|
|
45
|
+
supportsIfNotExists() { return false; }
|
|
46
|
+
supportsReturning() { return false; }
|
|
47
|
+
serializeBoolean(v) { return v; }
|
|
48
|
+
deserializeBoolean(v) {
|
|
49
|
+
return v === true || v === 1 || v === '1';
|
|
50
|
+
}
|
|
51
|
+
/** Spanner LIKE is case-sensitive — use LOWER() for case-insensitive search */
|
|
52
|
+
buildRegexCondition(col, flags) {
|
|
53
|
+
if (flags?.includes('i')) {
|
|
54
|
+
return `LOWER(${col}) LIKE LOWER(${this.nextPlaceholder()})`;
|
|
55
|
+
}
|
|
56
|
+
return `${col} LIKE ${this.nextPlaceholder()}`;
|
|
57
|
+
}
|
|
58
|
+
// Spanner supports LIMIT/OFFSET natively
|
|
59
|
+
// (default buildLimitOffset from AbstractSqlDialect works)
|
|
60
|
+
getCreateTablePrefix(tableName) {
|
|
61
|
+
return `CREATE TABLE ${this.quoteIdentifier(tableName)}`;
|
|
62
|
+
}
|
|
63
|
+
getCreateIndexPrefix(indexName, unique) {
|
|
64
|
+
const u = unique ? 'UNIQUE ' : '';
|
|
65
|
+
return `CREATE ${u}INDEX ${this.quoteIdentifier(indexName)}`;
|
|
66
|
+
}
|
|
67
|
+
// Spanner requires PRIMARY KEY as a separate clause, not inline
|
|
68
|
+
generateCreateTable(schema) {
|
|
69
|
+
const q = (name) => this.quoteIdentifier(name);
|
|
70
|
+
const cols = [` ${q('id')} ${this.getIdColumnType()} NOT NULL`];
|
|
71
|
+
for (const [name, field] of Object.entries(schema.fields)) {
|
|
72
|
+
let colDef = ` ${q(name)} ${this.fieldToSqlType(field)}`;
|
|
73
|
+
if (field.required)
|
|
74
|
+
colDef += ' NOT NULL';
|
|
75
|
+
// Spanner doesn't support UNIQUE in column definition — use unique index
|
|
76
|
+
// Spanner doesn't support DEFAULT
|
|
77
|
+
cols.push(colDef);
|
|
78
|
+
}
|
|
79
|
+
for (const [name, rel] of Object.entries(schema.relations)) {
|
|
80
|
+
if (rel.type === 'many-to-many')
|
|
81
|
+
continue;
|
|
82
|
+
if (rel.type === 'one-to-many') {
|
|
83
|
+
cols.push(` ${q(name)} ${this.fieldToSqlType({ type: 'json' })}`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
let colDef = ` ${q(name)} ${this.getIdColumnType()}`;
|
|
87
|
+
if (rel.required)
|
|
88
|
+
colDef += ' NOT NULL';
|
|
89
|
+
cols.push(colDef);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (schema.timestamps) {
|
|
93
|
+
cols.push(` ${q('createdAt')} ${this.fieldToSqlType({ type: 'date' })}`);
|
|
94
|
+
cols.push(` ${q('updatedAt')} ${this.fieldToSqlType({ type: 'date' })}`);
|
|
95
|
+
}
|
|
96
|
+
// Spanner: PRIMARY KEY is outside column definitions
|
|
97
|
+
return `CREATE TABLE ${q(schema.collection)} (\n${cols.join(',\n')}\n) PRIMARY KEY (${q('id')})`;
|
|
98
|
+
}
|
|
99
|
+
// --- Connection ---
|
|
100
|
+
async doConnect(config) {
|
|
101
|
+
try {
|
|
102
|
+
const spannerModule = await import(/* webpackIgnore: true */ '@google-cloud/spanner');
|
|
103
|
+
const Spanner = spannerModule.default?.Spanner || spannerModule.Spanner;
|
|
104
|
+
// URI format: spanner://project/instance/database
|
|
105
|
+
const parsed = this.parseSpannerUri(config.uri);
|
|
106
|
+
this.spannerClient = new Spanner({ projectId: parsed.projectId });
|
|
107
|
+
this.instance = this.spannerClient.instance(parsed.instanceId);
|
|
108
|
+
this.database = this.instance.database(parsed.databaseId);
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
throw new Error(`Google Cloud Spanner driver not found. Install it: npm install @google-cloud/spanner\n` +
|
|
112
|
+
`Original error: ${e instanceof Error ? e.message : String(e)}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async doDisconnect() {
|
|
116
|
+
if (this.database) {
|
|
117
|
+
await this.database.close();
|
|
118
|
+
this.database = null;
|
|
119
|
+
this.instance = null;
|
|
120
|
+
}
|
|
121
|
+
if (this.spannerClient) {
|
|
122
|
+
await this.spannerClient.close();
|
|
123
|
+
this.spannerClient = null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async doTestConnection() {
|
|
127
|
+
if (!this.database)
|
|
128
|
+
return false;
|
|
129
|
+
try {
|
|
130
|
+
const [rows] = await this.database.run({ sql: 'SELECT 1' });
|
|
131
|
+
return Array.isArray(rows);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// --- Query execution ---
|
|
138
|
+
async executeQuery(sql, params) {
|
|
139
|
+
if (!this.database)
|
|
140
|
+
throw new Error('Spanner not connected. Call connect() first.');
|
|
141
|
+
// Build named params object: { p1: val1, p2: val2, ... }
|
|
142
|
+
const namedParams = {};
|
|
143
|
+
for (let i = 0; i < params.length; i++) {
|
|
144
|
+
namedParams[`p${i + 1}`] = params[i];
|
|
145
|
+
}
|
|
146
|
+
const [rows] = await this.database.run({ sql, params: namedParams });
|
|
147
|
+
// Spanner returns Row objects — convert to plain objects
|
|
148
|
+
return rows.map(row => {
|
|
149
|
+
if (typeof row.toJSON === 'function') {
|
|
150
|
+
return row.toJSON();
|
|
151
|
+
}
|
|
152
|
+
return row;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
async executeRun(sql, params) {
|
|
156
|
+
if (!this.database)
|
|
157
|
+
throw new Error('Spanner not connected. Call connect() first.');
|
|
158
|
+
// For DML operations, Spanner requires using transactions
|
|
159
|
+
let changes = 0;
|
|
160
|
+
await this.database.runTransactionAsync(async (transaction) => {
|
|
161
|
+
const namedParams = {};
|
|
162
|
+
for (let i = 0; i < params.length; i++) {
|
|
163
|
+
namedParams[`p${i + 1}`] = params[i];
|
|
164
|
+
}
|
|
165
|
+
// DDL statements (CREATE TABLE, CREATE INDEX) go through updateSchema
|
|
166
|
+
if (sql.trimStart().toUpperCase().startsWith('CREATE') || sql.trimStart().toUpperCase().startsWith('DROP')) {
|
|
167
|
+
await transaction.commit();
|
|
168
|
+
await this.executeDdl(sql);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const [count] = await transaction.runUpdate({ sql, params: namedParams });
|
|
172
|
+
changes = count;
|
|
173
|
+
await transaction.commit();
|
|
174
|
+
});
|
|
175
|
+
return { changes };
|
|
176
|
+
}
|
|
177
|
+
/** Execute DDL statements (CREATE TABLE, etc.) via updateSchema */
|
|
178
|
+
async executeDdl(sql) {
|
|
179
|
+
const [operation] = await this.database.updateSchema([sql]);
|
|
180
|
+
await operation.promise();
|
|
181
|
+
}
|
|
182
|
+
// Override initSchema for Spanner's DDL requirements
|
|
183
|
+
async initSchema(schemas) {
|
|
184
|
+
this.schemas = schemas;
|
|
185
|
+
const strategy = this.config?.schemaStrategy ?? 'none';
|
|
186
|
+
this.log('INIT_SCHEMA', `strategy=${strategy}`, { entities: schemas.map(s => s.name) });
|
|
187
|
+
if (strategy === 'none')
|
|
188
|
+
return;
|
|
189
|
+
if (strategy === 'validate') {
|
|
190
|
+
for (const schema of schemas) {
|
|
191
|
+
const exists = await this.tableExists(schema.collection);
|
|
192
|
+
if (!exists) {
|
|
193
|
+
throw new Error(`Schema validation failed: table "${schema.collection}" does not exist ` +
|
|
194
|
+
`(entity: ${schema.name}). Set schemaStrategy to "update" or "create".`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
// Batch DDL statements for Spanner (more efficient)
|
|
200
|
+
const ddlStatements = [];
|
|
201
|
+
for (const schema of schemas) {
|
|
202
|
+
const exists = await this.tableExists(schema.collection);
|
|
203
|
+
if (!exists) {
|
|
204
|
+
ddlStatements.push(this.generateCreateTable(schema));
|
|
205
|
+
}
|
|
206
|
+
const indexStatements = this.generateIndexes(schema);
|
|
207
|
+
ddlStatements.push(...indexStatements);
|
|
208
|
+
}
|
|
209
|
+
// Junction tables
|
|
210
|
+
for (const schema of schemas) {
|
|
211
|
+
for (const [, rel] of Object.entries(schema.relations)) {
|
|
212
|
+
if (rel.type === 'many-to-many' && rel.through) {
|
|
213
|
+
const exists = await this.tableExists(rel.through);
|
|
214
|
+
if (exists)
|
|
215
|
+
continue;
|
|
216
|
+
const targetSchema = schemas.find(s => s.name === rel.target);
|
|
217
|
+
if (!targetSchema)
|
|
218
|
+
continue;
|
|
219
|
+
const sourceKey = `${schema.name.toLowerCase()}Id`;
|
|
220
|
+
const targetKey = `${rel.target.toLowerCase()}Id`;
|
|
221
|
+
const q = (n) => this.quoteIdentifier(n);
|
|
222
|
+
const idType = this.getIdColumnType();
|
|
223
|
+
ddlStatements.push(`CREATE TABLE ${q(rel.through)} (
|
|
224
|
+
${q(sourceKey)} ${idType} NOT NULL,
|
|
225
|
+
${q(targetKey)} ${idType} NOT NULL
|
|
226
|
+
) PRIMARY KEY (${q(sourceKey)}, ${q(targetKey)})`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (ddlStatements.length > 0) {
|
|
231
|
+
this.log('DDL_BATCH', 'all', ddlStatements);
|
|
232
|
+
try {
|
|
233
|
+
const [operation] = await this.database.updateSchema(ddlStatements);
|
|
234
|
+
await operation.promise();
|
|
235
|
+
}
|
|
236
|
+
catch (e) {
|
|
237
|
+
// Some statements may fail if objects already exist
|
|
238
|
+
this.log('DDL_BATCH_WARN', 'partial', e.message);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
parseSpannerUri(uri) {
|
|
243
|
+
// Format: spanner://project/instance/database
|
|
244
|
+
const cleaned = uri.replace(/^spanner:\/\//, '');
|
|
245
|
+
const parts = cleaned.split('/');
|
|
246
|
+
return {
|
|
247
|
+
projectId: parts[0] || 'my-project',
|
|
248
|
+
instanceId: parts[1] || 'my-instance',
|
|
249
|
+
databaseId: parts[2] || 'my-database',
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
getDialectLabel() { return 'Spanner'; }
|
|
253
|
+
}
|
|
254
|
+
// ============================================================
|
|
255
|
+
// Factory export
|
|
256
|
+
// ============================================================
|
|
257
|
+
export function createDialect() {
|
|
258
|
+
return new SpannerDialect();
|
|
259
|
+
}
|