@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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +548 -0
  3. package/dist/core/base-repository.d.ts +26 -0
  4. package/dist/core/base-repository.js +82 -0
  5. package/dist/core/config.d.ts +62 -0
  6. package/dist/core/config.js +116 -0
  7. package/dist/core/errors.d.ts +30 -0
  8. package/dist/core/errors.js +49 -0
  9. package/dist/core/factory.d.ts +41 -0
  10. package/dist/core/factory.js +142 -0
  11. package/dist/core/normalizer.d.ts +9 -0
  12. package/dist/core/normalizer.js +19 -0
  13. package/dist/core/registry.d.ts +43 -0
  14. package/dist/core/registry.js +78 -0
  15. package/dist/core/types.d.ts +228 -0
  16. package/dist/core/types.js +5 -0
  17. package/dist/dialects/abstract-sql.dialect.d.ts +113 -0
  18. package/dist/dialects/abstract-sql.dialect.js +1071 -0
  19. package/dist/dialects/cockroachdb.dialect.d.ts +2 -0
  20. package/dist/dialects/cockroachdb.dialect.js +23 -0
  21. package/dist/dialects/db2.dialect.d.ts +2 -0
  22. package/dist/dialects/db2.dialect.js +190 -0
  23. package/dist/dialects/hana.dialect.d.ts +2 -0
  24. package/dist/dialects/hana.dialect.js +199 -0
  25. package/dist/dialects/hsqldb.dialect.d.ts +2 -0
  26. package/dist/dialects/hsqldb.dialect.js +114 -0
  27. package/dist/dialects/mariadb.dialect.d.ts +2 -0
  28. package/dist/dialects/mariadb.dialect.js +87 -0
  29. package/dist/dialects/mongo.dialect.d.ts +2 -0
  30. package/dist/dialects/mongo.dialect.js +480 -0
  31. package/dist/dialects/mssql.dialect.d.ts +27 -0
  32. package/dist/dialects/mssql.dialect.js +127 -0
  33. package/dist/dialects/mysql.dialect.d.ts +24 -0
  34. package/dist/dialects/mysql.dialect.js +101 -0
  35. package/dist/dialects/oracle.dialect.d.ts +2 -0
  36. package/dist/dialects/oracle.dialect.js +206 -0
  37. package/dist/dialects/postgres.dialect.d.ts +26 -0
  38. package/dist/dialects/postgres.dialect.js +105 -0
  39. package/dist/dialects/spanner.dialect.d.ts +2 -0
  40. package/dist/dialects/spanner.dialect.js +259 -0
  41. package/dist/dialects/sqlite.dialect.d.ts +2 -0
  42. package/dist/dialects/sqlite.dialect.js +1027 -0
  43. package/dist/dialects/sybase.dialect.d.ts +2 -0
  44. package/dist/dialects/sybase.dialect.js +119 -0
  45. package/dist/index.d.ts +8 -0
  46. package/dist/index.js +26 -0
  47. package/docs/api-reference.md +1009 -0
  48. package/docs/dialects.md +673 -0
  49. package/docs/tutorial.md +846 -0
  50. 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
+ }
@@ -0,0 +1,2 @@
1
+ import type { IDialect } from '../core/types.js';
2
+ export declare function createDialect(): IDialect;