@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,480 @@
1
+ // MongoDB Dialect - Wraps Mongoose to implement IDialect
2
+ // Equivalent to org.hibernate.dialect.MongoDBDialect
3
+ // Author: Dr Hamid MADANI drmdh@msn.com
4
+ import mongoose, { Schema } from 'mongoose';
5
+ // ============================================================
6
+ // Model Registry — lazy-built from EntitySchema definitions
7
+ // ============================================================
8
+ const modelCache = new Map();
9
+ /**
10
+ * Get or build a Mongoose model from an EntitySchema.
11
+ * Models are cached per entity name (singleton per schema).
12
+ */
13
+ function getModel(schema) {
14
+ if (modelCache.has(schema.name)) {
15
+ return modelCache.get(schema.name);
16
+ }
17
+ // Return existing registered model if available
18
+ if (mongoose.models[schema.name]) {
19
+ modelCache.set(schema.name, mongoose.models[schema.name]);
20
+ return mongoose.models[schema.name];
21
+ }
22
+ const mongSchema = buildMongooseSchema(schema);
23
+ const model = mongoose.model(schema.name, mongSchema, schema.collection);
24
+ modelCache.set(schema.name, model);
25
+ return model;
26
+ }
27
+ /**
28
+ * Build a Mongoose Schema from an EntitySchema definition.
29
+ * Translates our generic FieldDef → Mongoose SchemaType.
30
+ */
31
+ function buildMongooseSchema(entity) {
32
+ const definition = {};
33
+ // --- Fields ---
34
+ for (const [name, field] of Object.entries(entity.fields)) {
35
+ const schemaDef = {};
36
+ // Type mapping
37
+ switch (field.type) {
38
+ case 'string':
39
+ schemaDef.type = String;
40
+ break;
41
+ case 'number':
42
+ schemaDef.type = Number;
43
+ break;
44
+ case 'boolean':
45
+ schemaDef.type = Boolean;
46
+ break;
47
+ case 'date':
48
+ schemaDef.type = Date;
49
+ break;
50
+ case 'json':
51
+ schemaDef.type = Schema.Types.Mixed;
52
+ break;
53
+ case 'array': {
54
+ if (!field.arrayOf) {
55
+ schemaDef.type = [Schema.Types.Mixed];
56
+ }
57
+ else if (typeof field.arrayOf === 'string') {
58
+ // Primitive array
59
+ const typeMap = {
60
+ string: String, number: Number, boolean: Boolean, date: Date,
61
+ };
62
+ schemaDef.type = [typeMap[field.arrayOf] || Schema.Types.Mixed];
63
+ }
64
+ else if (field.arrayOf.kind === 'embedded') {
65
+ // Embedded subdocument array
66
+ const subFields = {};
67
+ for (const [sf, sd] of Object.entries(field.arrayOf.fields)) {
68
+ const typeMap = {
69
+ string: String, number: Number, boolean: Boolean, date: Date,
70
+ };
71
+ subFields[sf] = {
72
+ type: typeMap[sd.type] || Schema.Types.Mixed,
73
+ ...(sd.required && { required: true }),
74
+ ...(sd.default !== undefined && { default: sd.default }),
75
+ };
76
+ }
77
+ schemaDef.type = [new Schema(subFields, { _id: false })];
78
+ }
79
+ if (field.default === undefined)
80
+ schemaDef.default = undefined;
81
+ break;
82
+ }
83
+ }
84
+ if (field.required)
85
+ schemaDef.required = true;
86
+ if (field.unique)
87
+ schemaDef.unique = true;
88
+ if (field.sparse)
89
+ schemaDef.sparse = true;
90
+ if (field.lowercase)
91
+ schemaDef.lowercase = true;
92
+ if (field.trim)
93
+ schemaDef.trim = true;
94
+ if (field.enum)
95
+ schemaDef.enum = field.enum;
96
+ if (field.default !== undefined && field.type !== 'array') {
97
+ schemaDef.default = field.default === 'now' ? Date.now : field.default;
98
+ }
99
+ definition[name] = schemaDef;
100
+ }
101
+ // --- Relations → ObjectId refs ---
102
+ for (const [name, rel] of Object.entries(entity.relations)) {
103
+ if (rel.type === 'one-to-many' || rel.type === 'many-to-many') {
104
+ // Array of refs (e.g. Role.permissions, User.roles)
105
+ definition[name] = [{ type: Schema.Types.ObjectId, ref: rel.target }];
106
+ }
107
+ else {
108
+ // many-to-one or one-to-one → single ref
109
+ definition[name] = {
110
+ type: Schema.Types.ObjectId,
111
+ ref: rel.target,
112
+ ...(rel.required && { required: true }),
113
+ ...(rel.nullable && { default: null }),
114
+ };
115
+ }
116
+ }
117
+ const mongoSchema = new Schema(definition, {
118
+ timestamps: entity.timestamps,
119
+ collection: entity.collection,
120
+ });
121
+ // --- Indexes ---
122
+ for (const idx of entity.indexes) {
123
+ const mongoIndex = {};
124
+ for (const [field, dir] of Object.entries(idx.fields)) {
125
+ if (dir === 'text')
126
+ mongoIndex[field] = 'text';
127
+ else if (dir === 'desc')
128
+ mongoIndex[field] = -1;
129
+ else
130
+ mongoIndex[field] = 1;
131
+ }
132
+ mongoSchema.index(mongoIndex, {
133
+ ...(idx.unique && { unique: true }),
134
+ ...(idx.sparse && { sparse: true }),
135
+ });
136
+ }
137
+ return mongoSchema;
138
+ }
139
+ // ============================================================
140
+ // Query Translation — DAL FilterQuery → Mongoose filter
141
+ // ============================================================
142
+ /**
143
+ * Translate a DAL FilterQuery to a Mongoose-compatible filter object.
144
+ */
145
+ function translateFilter(filter) {
146
+ const result = {};
147
+ for (const [key, value] of Object.entries(filter)) {
148
+ if (key === '$or' && Array.isArray(value)) {
149
+ result.$or = value.map(translateFilter);
150
+ }
151
+ else if (key === '$and' && Array.isArray(value)) {
152
+ result.$and = value.map(translateFilter);
153
+ }
154
+ else if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
155
+ // FilterOperator
156
+ const op = value;
157
+ const mongoOp = {};
158
+ if ('$eq' in op)
159
+ mongoOp.$eq = op.$eq;
160
+ if ('$ne' in op)
161
+ mongoOp.$ne = op.$ne;
162
+ if ('$gt' in op)
163
+ mongoOp.$gt = op.$gt;
164
+ if ('$gte' in op)
165
+ mongoOp.$gte = op.$gte;
166
+ if ('$lt' in op)
167
+ mongoOp.$lt = op.$lt;
168
+ if ('$lte' in op)
169
+ mongoOp.$lte = op.$lte;
170
+ if ('$in' in op)
171
+ mongoOp.$in = op.$in;
172
+ if ('$nin' in op)
173
+ mongoOp.$nin = op.$nin;
174
+ if ('$exists' in op)
175
+ mongoOp.$exists = op.$exists;
176
+ if ('$regex' in op) {
177
+ mongoOp.$regex = op.$regex;
178
+ if (op.$regexFlags)
179
+ mongoOp.$options = op.$regexFlags;
180
+ }
181
+ // If no operator keys matched, pass through as-is (raw Mongoose filter)
182
+ result[key] = Object.keys(mongoOp).length > 0 ? mongoOp : value;
183
+ }
184
+ else {
185
+ result[key] = value;
186
+ }
187
+ }
188
+ return result;
189
+ }
190
+ /**
191
+ * Apply QueryOptions to a Mongoose query chain.
192
+ */
193
+ function applyOptions(query, options) {
194
+ if (!options)
195
+ return query;
196
+ if (options.sort)
197
+ query = query.sort(options.sort);
198
+ if (options.skip)
199
+ query = query.skip(options.skip);
200
+ if (options.limit)
201
+ query = query.limit(options.limit);
202
+ if (options.select)
203
+ query = query.select(options.select.join(' '));
204
+ if (options.exclude)
205
+ query = query.select(options.exclude.map((f) => `-${f}`).join(' '));
206
+ return query;
207
+ }
208
+ // ============================================================
209
+ // SQL Logging — inspired by hibernate.show_sql / hibernate.format_sql
210
+ // ============================================================
211
+ let showSql = false;
212
+ let formatSql = false;
213
+ function logQuery(operation, collection, details) {
214
+ if (!showSql)
215
+ return;
216
+ const prefix = `[DAL:MongoDB] ${operation} ${collection}`;
217
+ if (formatSql && details) {
218
+ console.log(prefix);
219
+ console.log(JSON.stringify(details, null, 2));
220
+ }
221
+ else if (details) {
222
+ console.log(`${prefix} ${JSON.stringify(details)}`);
223
+ }
224
+ else {
225
+ console.log(prefix);
226
+ }
227
+ }
228
+ // ============================================================
229
+ // MongoDialect — implements IDialect
230
+ // ============================================================
231
+ class MongoDialect {
232
+ dialectType = 'mongodb';
233
+ config = null;
234
+ async connect(config) {
235
+ this.config = config;
236
+ showSql = config.showSql ?? false;
237
+ formatSql = config.formatSql ?? false;
238
+ const options = {
239
+ bufferCommands: false,
240
+ };
241
+ // hibernate.connection.pool_size equivalent
242
+ if (config.poolSize) {
243
+ options.maxPoolSize = config.poolSize;
244
+ }
245
+ // Reuse existing connection if already connected
246
+ if (mongoose.connection.readyState === 1) {
247
+ logQuery('REUSE', 'connection');
248
+ return;
249
+ }
250
+ await mongoose.connect(config.uri, options);
251
+ logQuery('CONNECT', config.uri.replace(/\/\/.*@/, '//<credentials>@'));
252
+ // hibernate.hbm2ddl.auto equivalent
253
+ if (config.schemaStrategy === 'create') {
254
+ logQuery('SCHEMA', 'create — dropping existing collections');
255
+ await mongoose.connection.db.dropDatabase();
256
+ }
257
+ }
258
+ async disconnect() {
259
+ // hibernate.hbm2ddl.auto=create-drop
260
+ if (this.config?.schemaStrategy === 'create-drop') {
261
+ logQuery('SCHEMA', 'create-drop — dropping database on shutdown');
262
+ await mongoose.connection.db.dropDatabase();
263
+ }
264
+ modelCache.clear();
265
+ await mongoose.disconnect();
266
+ logQuery('DISCONNECT', '');
267
+ }
268
+ async testConnection() {
269
+ try {
270
+ if (mongoose.connection.readyState !== 1)
271
+ return false;
272
+ await mongoose.connection.db.admin().ping();
273
+ return true;
274
+ }
275
+ catch {
276
+ return false;
277
+ }
278
+ }
279
+ // --- Schema management (hibernate.hbm2ddl.auto) ---
280
+ async initSchema(schemas) {
281
+ const strategy = this.config?.schemaStrategy ?? 'none';
282
+ logQuery('INIT_SCHEMA', `strategy=${strategy}`, { entities: schemas.map(s => s.name) });
283
+ for (const schema of schemas) {
284
+ // Always register models so .populate() can resolve refs (User→Role→Permission)
285
+ const model = getModel(schema);
286
+ if (strategy === 'update' || strategy === 'create') {
287
+ // Ensure indexes exist (like hbm2ddl.auto=update)
288
+ await model.ensureIndexes();
289
+ }
290
+ if (strategy === 'validate') {
291
+ // Validate that collection exists
292
+ const collections = await mongoose.connection.db.listCollections({ name: schema.collection }).toArray();
293
+ if (collections.length === 0) {
294
+ throw new Error(`Schema validation failed: collection "${schema.collection}" does not exist ` +
295
+ `(entity: ${schema.name}). Set schemaStrategy to "update" or "create".`);
296
+ }
297
+ }
298
+ }
299
+ }
300
+ // --- CRUD ---
301
+ async find(schema, filter, options) {
302
+ const model = getModel(schema);
303
+ const mongoFilter = translateFilter(filter);
304
+ logQuery('FIND', schema.collection, { filter: mongoFilter, options });
305
+ let query = model.find(mongoFilter);
306
+ query = applyOptions(query, options);
307
+ return query.lean();
308
+ }
309
+ async findOne(schema, filter, options) {
310
+ const model = getModel(schema);
311
+ const mongoFilter = translateFilter(filter);
312
+ logQuery('FIND_ONE', schema.collection, { filter: mongoFilter });
313
+ let query = model.findOne(mongoFilter);
314
+ query = applyOptions(query, options);
315
+ return query.lean();
316
+ }
317
+ async findById(schema, id, options) {
318
+ const model = getModel(schema);
319
+ logQuery('FIND_BY_ID', schema.collection, { id });
320
+ let query = model.findById(id);
321
+ query = applyOptions(query, options);
322
+ return query.lean();
323
+ }
324
+ async create(schema, data) {
325
+ const model = getModel(schema);
326
+ logQuery('CREATE', schema.collection, data);
327
+ const doc = await model.create(data);
328
+ return doc.toObject();
329
+ }
330
+ async update(schema, id, data) {
331
+ const model = getModel(schema);
332
+ logQuery('UPDATE', schema.collection, { id, data });
333
+ return model.findByIdAndUpdate(id, data, { returnDocument: 'after' }).lean();
334
+ }
335
+ async updateMany(schema, filter, data) {
336
+ const model = getModel(schema);
337
+ const mongoFilter = translateFilter(filter);
338
+ logQuery('UPDATE_MANY', schema.collection, { filter: mongoFilter, data });
339
+ const result = await model.updateMany(mongoFilter, data);
340
+ return result.modifiedCount;
341
+ }
342
+ async delete(schema, id) {
343
+ const model = getModel(schema);
344
+ logQuery('DELETE', schema.collection, { id });
345
+ const result = await model.findByIdAndDelete(id);
346
+ return result !== null;
347
+ }
348
+ async deleteMany(schema, filter) {
349
+ const model = getModel(schema);
350
+ const mongoFilter = translateFilter(filter);
351
+ logQuery('DELETE_MANY', schema.collection, { filter: mongoFilter });
352
+ const result = await model.deleteMany(mongoFilter);
353
+ return result.deletedCount;
354
+ }
355
+ // --- Queries ---
356
+ async count(schema, filter) {
357
+ const model = getModel(schema);
358
+ const mongoFilter = translateFilter(filter);
359
+ logQuery('COUNT', schema.collection, { filter: mongoFilter });
360
+ return model.countDocuments(mongoFilter);
361
+ }
362
+ async distinct(schema, field, filter) {
363
+ const model = getModel(schema);
364
+ const mongoFilter = translateFilter(filter);
365
+ logQuery('DISTINCT', schema.collection, { field, filter: mongoFilter });
366
+ return model.distinct(field, mongoFilter);
367
+ }
368
+ async aggregate(schema, stages) {
369
+ const model = getModel(schema);
370
+ // Translate our aggregate stages to Mongoose pipeline
371
+ const pipeline = stages.map(stage => {
372
+ if ('$match' in stage) {
373
+ return { $match: translateFilter(stage.$match) };
374
+ }
375
+ if ('$group' in stage) {
376
+ const group = {};
377
+ for (const [key, val] of Object.entries(stage.$group)) {
378
+ if (key === '_by') {
379
+ group._id = val ? `$${val}` : null;
380
+ }
381
+ else {
382
+ group[key] = val;
383
+ }
384
+ }
385
+ return { $group: group };
386
+ }
387
+ if ('$sort' in stage) {
388
+ return { $sort: stage.$sort };
389
+ }
390
+ if ('$limit' in stage) {
391
+ return { $limit: stage.$limit };
392
+ }
393
+ return stage;
394
+ });
395
+ logQuery('AGGREGATE', schema.collection, pipeline);
396
+ return model.aggregate(pipeline);
397
+ }
398
+ // --- Relations (equivalent Hibernate eager/lazy loading via populate) ---
399
+ async findWithRelations(schema, filter, relations, options) {
400
+ const model = getModel(schema);
401
+ const mongoFilter = translateFilter(filter);
402
+ logQuery('FIND_WITH_RELATIONS', schema.collection, { filter: mongoFilter, relations });
403
+ let query = model.find(mongoFilter);
404
+ query = applyOptions(query, options);
405
+ for (const rel of relations) {
406
+ const relDef = schema.relations[rel];
407
+ if (relDef?.select) {
408
+ query = query.populate(rel, relDef.select.join(' '));
409
+ }
410
+ else {
411
+ query = query.populate(rel);
412
+ }
413
+ }
414
+ return query.lean();
415
+ }
416
+ async findByIdWithRelations(schema, id, relations, options) {
417
+ const model = getModel(schema);
418
+ logQuery('FIND_BY_ID_WITH_RELATIONS', schema.collection, { id, relations });
419
+ let query = model.findById(id);
420
+ query = applyOptions(query, options);
421
+ for (const rel of relations) {
422
+ const relDef = schema.relations[rel];
423
+ if (relDef?.select) {
424
+ query = query.populate(rel, relDef.select.join(' '));
425
+ }
426
+ else {
427
+ query = query.populate(rel);
428
+ }
429
+ }
430
+ return query.lean();
431
+ }
432
+ // --- Upsert (equivalent Hibernate saveOrUpdate / merge) ---
433
+ async upsert(schema, filter, data) {
434
+ const model = getModel(schema);
435
+ const mongoFilter = translateFilter(filter);
436
+ logQuery('UPSERT', schema.collection, { filter: mongoFilter, data });
437
+ const result = await model.findOneAndUpdate(mongoFilter, data, {
438
+ upsert: true,
439
+ returnDocument: 'after',
440
+ }).lean();
441
+ return result;
442
+ }
443
+ // --- Atomic operations ---
444
+ async increment(schema, id, field, amount) {
445
+ const model = getModel(schema);
446
+ logQuery('INCREMENT', schema.collection, { id, field, amount });
447
+ const result = await model.findByIdAndUpdate(id, { $inc: { [field]: amount } }, { returnDocument: 'after', upsert: true }).lean();
448
+ return result;
449
+ }
450
+ // --- Array operations (equivalent Hibernate @ElementCollection management) ---
451
+ async addToSet(schema, id, field, value) {
452
+ const model = getModel(schema);
453
+ logQuery('ADD_TO_SET', schema.collection, { id, field, value });
454
+ return model.findByIdAndUpdate(id, { $addToSet: { [field]: value } }, { returnDocument: 'after' }).lean();
455
+ }
456
+ async pull(schema, id, field, value) {
457
+ const model = getModel(schema);
458
+ logQuery('PULL', schema.collection, { id, field, value });
459
+ return model.findByIdAndUpdate(id, { $pull: { [field]: value } }, { returnDocument: 'after' }).lean();
460
+ }
461
+ // --- Text search ---
462
+ async search(schema, query, fields, options) {
463
+ const model = getModel(schema);
464
+ // Build $or with $regex for each field (works with or without text index)
465
+ const orConditions = fields.map(field => ({
466
+ [field]: { $regex: query, $options: 'i' },
467
+ }));
468
+ const filter = { $or: orConditions };
469
+ logQuery('SEARCH', schema.collection, { query, fields, filter });
470
+ let q = model.find(filter);
471
+ q = applyOptions(q, options);
472
+ return q.lean();
473
+ }
474
+ }
475
+ // ============================================================
476
+ // Factory export
477
+ // ============================================================
478
+ export function createDialect() {
479
+ return new MongoDialect();
480
+ }
@@ -0,0 +1,27 @@
1
+ import type { IDialect, DialectType, ConnectionConfig, FieldDef, QueryOptions } from '../core/types.js';
2
+ import { AbstractSqlDialect } from './abstract-sql.dialect.js';
3
+ export declare class MSSQLDialect extends AbstractSqlDialect {
4
+ readonly dialectType: DialectType;
5
+ protected pool: unknown;
6
+ quoteIdentifier(name: string): string;
7
+ getPlaceholder(index: number): string;
8
+ fieldToSqlType(field: FieldDef): string;
9
+ getIdColumnType(): string;
10
+ getTableListQuery(): string;
11
+ protected supportsIfNotExists(): boolean;
12
+ protected supportsReturning(): boolean;
13
+ protected serializeBoolean(v: boolean): unknown;
14
+ protected deserializeBoolean(v: unknown): boolean;
15
+ protected buildLimitOffset(options?: QueryOptions): string;
16
+ protected getCreateTablePrefix(tableName: string): string;
17
+ protected getCreateIndexPrefix(indexName: string, unique: boolean): string;
18
+ doConnect(config: ConnectionConfig): Promise<void>;
19
+ doDisconnect(): Promise<void>;
20
+ doTestConnection(): Promise<boolean>;
21
+ executeQuery<T>(sql: string, params: unknown[]): Promise<T[]>;
22
+ executeRun(sql: string, params: unknown[]): Promise<{
23
+ changes: number;
24
+ }>;
25
+ protected getDialectLabel(): string;
26
+ }
27
+ export declare function createDialect(): IDialect;
@@ -0,0 +1,127 @@
1
+ // Microsoft SQL Server Dialect — extends AbstractSqlDialect
2
+ // Equivalent to org.hibernate.dialect.SQLServerDialect (Hibernate ORM 6.4)
3
+ // Driver: npm install mssql
4
+ // Author: Dr Hamid MADANI drmdh@msn.com
5
+ import { AbstractSqlDialect } from './abstract-sql.dialect.js';
6
+ // ============================================================
7
+ // Type Mapping — DAL FieldType → SQL Server column type
8
+ // ============================================================
9
+ const MSSQL_TYPE_MAP = {
10
+ string: 'NVARCHAR(MAX)',
11
+ number: 'FLOAT',
12
+ boolean: 'BIT',
13
+ date: 'DATETIME2',
14
+ json: 'NVARCHAR(MAX)',
15
+ array: 'NVARCHAR(MAX)',
16
+ };
17
+ // ============================================================
18
+ // MSSQLDialect
19
+ // ============================================================
20
+ export class MSSQLDialect extends AbstractSqlDialect {
21
+ dialectType = 'mssql';
22
+ pool = null;
23
+ // --- Abstract implementations ---
24
+ quoteIdentifier(name) {
25
+ return `[${name}]`;
26
+ }
27
+ getPlaceholder(index) {
28
+ return `@p${index}`;
29
+ }
30
+ fieldToSqlType(field) {
31
+ return MSSQL_TYPE_MAP[field.type] || 'NVARCHAR(MAX)';
32
+ }
33
+ getIdColumnType() {
34
+ return 'NVARCHAR(36)';
35
+ }
36
+ getTableListQuery() {
37
+ return "SELECT name FROM sys.tables WHERE type = 'U'";
38
+ }
39
+ // --- Hooks ---
40
+ supportsIfNotExists() { return false; }
41
+ // SQL Server supports OUTPUT clause (similar to RETURNING)
42
+ supportsReturning() { return true; }
43
+ // SQL Server BIT: 1 = true, 0 = false
44
+ serializeBoolean(v) { return v ? 1 : 0; }
45
+ deserializeBoolean(v) {
46
+ return v === 1 || v === true || v === '1';
47
+ }
48
+ // SQL Server uses OFFSET/FETCH instead of LIMIT/OFFSET
49
+ buildLimitOffset(options) {
50
+ if (!options?.limit && !options?.skip)
51
+ return '';
52
+ // SQL Server requires ORDER BY for OFFSET/FETCH
53
+ // If no ORDER BY was specified, use (SELECT NULL) as a workaround
54
+ const offset = options.skip ?? 0;
55
+ const limit = options.limit;
56
+ let sql = ` OFFSET ${offset} ROWS`;
57
+ if (limit)
58
+ sql += ` FETCH NEXT ${limit} ROWS ONLY`;
59
+ return sql;
60
+ }
61
+ // Override: SQL Server needs ORDER BY before OFFSET/FETCH
62
+ getCreateTablePrefix(tableName) {
63
+ const q = this.quoteIdentifier(tableName);
64
+ // SQL Server 2016+ supports IF NOT EXISTS via a different pattern
65
+ return `IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = '${tableName}') CREATE TABLE ${q}`;
66
+ }
67
+ getCreateIndexPrefix(indexName, unique) {
68
+ const u = unique ? 'UNIQUE ' : '';
69
+ const q = this.quoteIdentifier(indexName);
70
+ // SQL Server: check if index exists before creating
71
+ return `IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = '${indexName}') CREATE ${u}INDEX ${q}`;
72
+ }
73
+ // --- Connection ---
74
+ async doConnect(config) {
75
+ try {
76
+ const mssql = await import(/* webpackIgnore: true */ 'mssql');
77
+ const connect = mssql.default?.connect || mssql.connect;
78
+ this.pool = await connect(config.uri);
79
+ }
80
+ catch (e) {
81
+ throw new Error(`SQL Server driver not found. Install it: npm install mssql\n` +
82
+ `Original error: ${e instanceof Error ? e.message : String(e)}`);
83
+ }
84
+ }
85
+ async doDisconnect() {
86
+ if (this.pool) {
87
+ await this.pool.close();
88
+ this.pool = null;
89
+ }
90
+ }
91
+ async doTestConnection() {
92
+ if (!this.pool)
93
+ return false;
94
+ const request = this.pool.request();
95
+ await request.query('SELECT 1');
96
+ return true;
97
+ }
98
+ // --- Query execution ---
99
+ async executeQuery(sql, params) {
100
+ if (!this.pool)
101
+ throw new Error('SQL Server not connected. Call connect() first.');
102
+ const request = this.pool.request();
103
+ // Bind named parameters @p1, @p2, ...
104
+ for (let i = 0; i < params.length; i++) {
105
+ request.input(`p${i + 1}`, params[i]);
106
+ }
107
+ const result = await request.query(sql);
108
+ return result.recordset;
109
+ }
110
+ async executeRun(sql, params) {
111
+ if (!this.pool)
112
+ throw new Error('SQL Server not connected. Call connect() first.');
113
+ const request = this.pool.request();
114
+ for (let i = 0; i < params.length; i++) {
115
+ request.input(`p${i + 1}`, params[i]);
116
+ }
117
+ const result = await request.query(sql);
118
+ return { changes: result.rowsAffected?.[0] ?? 0 };
119
+ }
120
+ getDialectLabel() { return 'MSSQL'; }
121
+ }
122
+ // ============================================================
123
+ // Factory export
124
+ // ============================================================
125
+ export function createDialect() {
126
+ return new MSSQLDialect();
127
+ }
@@ -0,0 +1,24 @@
1
+ import type { IDialect, DialectType, ConnectionConfig, FieldDef } from '../core/types.js';
2
+ import { AbstractSqlDialect } from './abstract-sql.dialect.js';
3
+ export declare class MySQLDialect extends AbstractSqlDialect {
4
+ readonly dialectType: DialectType;
5
+ protected pool: unknown;
6
+ quoteIdentifier(name: string): string;
7
+ getPlaceholder(_index: number): string;
8
+ fieldToSqlType(field: FieldDef): string;
9
+ getIdColumnType(): string;
10
+ getTableListQuery(): string;
11
+ protected supportsIfNotExists(): boolean;
12
+ protected supportsReturning(): boolean;
13
+ protected serializeBoolean(v: boolean): unknown;
14
+ protected deserializeBoolean(v: unknown): boolean;
15
+ doConnect(config: ConnectionConfig): Promise<void>;
16
+ doDisconnect(): Promise<void>;
17
+ doTestConnection(): Promise<boolean>;
18
+ executeQuery<T>(sql: string, params: unknown[]): Promise<T[]>;
19
+ executeRun(sql: string, params: unknown[]): Promise<{
20
+ changes: number;
21
+ }>;
22
+ protected getDialectLabel(): string;
23
+ }
24
+ export declare function createDialect(): IDialect;