@promakeai/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.
@@ -0,0 +1,38 @@
1
+ /**
2
+ * MongoDB-style Where Clause Builder
3
+ *
4
+ * Converts MongoDB/Mongoose-style query objects into SQL WHERE clauses
5
+ * with parameterized values.
6
+ *
7
+ * Supported operators:
8
+ * - Comparison: $eq, $ne, $gt, $gte, $lt, $lte
9
+ * - Array: $in, $nin
10
+ * - String: $like, $notLike
11
+ * - Range: $between
12
+ * - Null: $isNull
13
+ * - Negation: $not (field-level)
14
+ * - Logical: $and, $or, $nor
15
+ */
16
+ export interface WhereResult {
17
+ sql: string;
18
+ params: unknown[];
19
+ }
20
+ /**
21
+ * Build a SQL WHERE clause from a MongoDB-style query object.
22
+ *
23
+ * @example
24
+ * // Simple equality
25
+ * buildWhereClause({ active: 1 })
26
+ * // => { sql: "active = ?", params: [1] }
27
+ *
28
+ * @example
29
+ * // Comparison operators
30
+ * buildWhereClause({ age: { $gte: 18, $lt: 65 } })
31
+ * // => { sql: "(age >= ? AND age < ?)", params: [18, 65] }
32
+ *
33
+ * @example
34
+ * // Logical operators
35
+ * buildWhereClause({ $or: [{ active: 1 }, { role: "admin" }] })
36
+ * // => { sql: "(active = ? OR role = ?)", params: [1, "admin"] }
37
+ */
38
+ export declare function buildWhereClause(where?: Record<string, unknown>): WhereResult;
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@promakeai/orm",
3
+ "version": "1.0.0",
4
+ "description": "Database-agnostic ORM core - works in browser and Node.js",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./schema": {
16
+ "types": "./dist/schema/index.d.ts",
17
+ "import": "./dist/schema/index.js"
18
+ }
19
+ },
20
+ "scripts": {
21
+ "build": "bun build src/index.ts --outdir dist --target browser --format esm && bun x tsc --emitDeclarationOnly",
22
+ "dev": "bun build src/index.ts --outdir dist --target browser --format esm --watch",
23
+ "test": "bun test",
24
+ "typecheck": "bun x tsc --noEmit"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "src"
29
+ ],
30
+ "keywords": [
31
+ "orm",
32
+ "database",
33
+ "adapter",
34
+ "typescript",
35
+ "browser",
36
+ "nodejs",
37
+ "sql",
38
+ "nosql",
39
+ "rest"
40
+ ],
41
+ "author": "Promake Inc.",
42
+ "devDependencies": {
43
+ "typescript": "^5.8.0"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ }
48
+ }
package/src/ORM.ts ADDED
@@ -0,0 +1,398 @@
1
+ /**
2
+ * ORM Class - Main Entry Point
3
+ *
4
+ * Database-agnostic ORM that works with any adapter implementing IDataAdapter.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { ORM } from '@promakeai/orm';
9
+ * import { SqliteAdapter } from '@promakeai/dbcli';
10
+ *
11
+ * const adapter = new SqliteAdapter('./database.db');
12
+ * const db = new ORM(adapter);
13
+ *
14
+ * // List with filters and populate
15
+ * const posts = await db.list('posts', {
16
+ * where: { published: true },
17
+ * populate: ['userId', 'categoryIds'],
18
+ * orderBy: [{ field: 'created_at', direction: 'DESC' }],
19
+ * limit: 10,
20
+ * });
21
+ *
22
+ * // Create
23
+ * const post = await db.create('posts', { title: 'Hello', published: true });
24
+ *
25
+ * // Transaction
26
+ * await db.transaction(async (tx) => {
27
+ * await tx.create('posts', { title: 'Post 1' });
28
+ * await tx.create('posts', { title: 'Post 2' });
29
+ * });
30
+ * ```
31
+ */
32
+
33
+ import type { IDataAdapter } from "./adapters/IDataAdapter";
34
+ import type {
35
+ QueryOptions,
36
+ PaginatedResult,
37
+ SchemaDefinition,
38
+ ORMConfig,
39
+ } from "./types";
40
+ import { resolvePopulate, validatePopulate } from "./utils/populateResolver";
41
+
42
+ /**
43
+ * Main ORM class - Completely adapter agnostic
44
+ */
45
+ export class ORM {
46
+ private adapter: IDataAdapter;
47
+ private schema?: SchemaDefinition;
48
+ private defaultLang: string;
49
+ private fallbackLang?: string;
50
+
51
+ /**
52
+ * Create ORM instance with any adapter
53
+ *
54
+ * @param adapter - Any object implementing IDataAdapter interface
55
+ * @param config - Optional configuration
56
+ */
57
+ constructor(adapter: IDataAdapter, config?: ORMConfig) {
58
+ this.adapter = adapter;
59
+ this.schema = config?.schema;
60
+ this.defaultLang = config?.defaultLang || "en";
61
+ this.fallbackLang = config?.fallbackLang;
62
+ }
63
+
64
+ // ==================== Query Methods ====================
65
+
66
+ /**
67
+ * List records from table
68
+ *
69
+ * @example
70
+ * ```typescript
71
+ * // Simple list
72
+ * const products = await db.list('products');
73
+ *
74
+ * // With populate
75
+ * const products = await db.list('products', {
76
+ * where: { active: true },
77
+ * populate: ['categoryId', 'brandId'],
78
+ * limit: 10,
79
+ * });
80
+ * // Each product has .category and .brand objects
81
+ *
82
+ * // Nested populate
83
+ * const orders = await db.list('orders', {
84
+ * populate: [
85
+ * 'userId',
86
+ * { path: 'items', populate: ['productId'] }
87
+ * ]
88
+ * });
89
+ * ```
90
+ */
91
+ async list<T = unknown>(
92
+ table: string,
93
+ options?: QueryOptions
94
+ ): Promise<T[]> {
95
+ const opts = this.normalizeOptions(options);
96
+ const { populate, ...queryOpts } = opts;
97
+
98
+ // Fetch records
99
+ let records = await this.adapter.list<T>(table, queryOpts);
100
+
101
+ // Resolve populate if schema is available
102
+ if (populate && this.schema) {
103
+ records = await this.resolvePopulateForRecords(records, table, populate);
104
+ }
105
+
106
+ return records;
107
+ }
108
+
109
+ /**
110
+ * Get single record by ID
111
+ *
112
+ * @example
113
+ * ```typescript
114
+ * const product = await db.get('products', 1, {
115
+ * populate: ['categoryId']
116
+ * });
117
+ * console.log(product.category.name);
118
+ * ```
119
+ */
120
+ async get<T = unknown>(
121
+ table: string,
122
+ id: string | number,
123
+ options?: QueryOptions
124
+ ): Promise<T | null> {
125
+ const opts = this.normalizeOptions(options);
126
+ const { populate, ...queryOpts } = opts;
127
+
128
+ const record = await this.adapter.get<T>(table, id, queryOpts);
129
+
130
+ // Resolve populate if record found and schema is available
131
+ if (record && populate && this.schema) {
132
+ const [populated] = await this.resolvePopulateForRecords(
133
+ [record],
134
+ table,
135
+ populate
136
+ );
137
+ return populated;
138
+ }
139
+
140
+ return record;
141
+ }
142
+
143
+ /**
144
+ * Count records
145
+ */
146
+ async count(table: string, options?: QueryOptions): Promise<number> {
147
+ const opts = this.normalizeOptions(options);
148
+ return this.adapter.count(table, opts);
149
+ }
150
+
151
+ /**
152
+ * Paginated query
153
+ */
154
+ async paginate<T = unknown>(
155
+ table: string,
156
+ page: number,
157
+ limit: number,
158
+ options?: QueryOptions
159
+ ): Promise<PaginatedResult<T>> {
160
+ const opts = this.normalizeOptions(options);
161
+ return this.adapter.paginate<T>(table, page, limit, opts);
162
+ }
163
+
164
+ // ==================== Write Methods ====================
165
+
166
+ /**
167
+ * Create new record
168
+ */
169
+ async create<T = unknown>(
170
+ table: string,
171
+ data: Record<string, unknown>
172
+ ): Promise<T> {
173
+ // Validate if schema exists
174
+ if (this.schema) {
175
+ this.validateData(table, data);
176
+ }
177
+ return this.adapter.create<T>(table, data);
178
+ }
179
+
180
+ /**
181
+ * Update record by ID
182
+ */
183
+ async update<T = unknown>(
184
+ table: string,
185
+ id: string | number,
186
+ data: Record<string, unknown>
187
+ ): Promise<T> {
188
+ // Validate if schema exists
189
+ if (this.schema) {
190
+ this.validateData(table, data, true);
191
+ }
192
+ return this.adapter.update<T>(table, id, data);
193
+ }
194
+
195
+ /**
196
+ * Delete record by ID
197
+ */
198
+ async delete(table: string, id: string | number): Promise<boolean> {
199
+ return this.adapter.delete(table, id);
200
+ }
201
+
202
+ // ==================== Batch Methods ====================
203
+
204
+ /**
205
+ * Create multiple records
206
+ */
207
+ async createMany<T = unknown>(
208
+ table: string,
209
+ records: Record<string, unknown>[],
210
+ options?: { ignore?: boolean }
211
+ ): Promise<{ created: number; ids: (number | bigint)[] }> {
212
+ return this.adapter.createMany<T>(table, records, options);
213
+ }
214
+
215
+ /**
216
+ * Update multiple records
217
+ */
218
+ async updateMany(
219
+ table: string,
220
+ updates: { id: number | string; data: Record<string, unknown> }[]
221
+ ): Promise<{ updated: number }> {
222
+ return this.adapter.updateMany(table, updates);
223
+ }
224
+
225
+ /**
226
+ * Delete multiple records
227
+ */
228
+ async deleteMany(
229
+ table: string,
230
+ ids: (number | string)[]
231
+ ): Promise<{ deleted: number }> {
232
+ return this.adapter.deleteMany(table, ids);
233
+ }
234
+
235
+ // ==================== Raw Query Methods ====================
236
+
237
+ /**
238
+ * Execute raw query
239
+ */
240
+ async raw<T = unknown>(query: string, params?: unknown[]): Promise<T[]> {
241
+ return this.adapter.raw<T>(query, params);
242
+ }
243
+
244
+ /**
245
+ * Execute raw statement
246
+ */
247
+ async execute(
248
+ query: string,
249
+ params?: unknown[]
250
+ ): Promise<{ changes: number; lastInsertRowid: number | bigint }> {
251
+ return this.adapter.execute(query, params);
252
+ }
253
+
254
+ // ==================== Transaction ====================
255
+
256
+ /**
257
+ * Transaction wrapper
258
+ *
259
+ * @example
260
+ * ```typescript
261
+ * await db.transaction(async (tx) => {
262
+ * const user = await tx.create('users', { name: 'John' });
263
+ * await tx.create('posts', { title: 'Hello', userId: user.id });
264
+ * });
265
+ * ```
266
+ */
267
+ async transaction<T>(callback: (orm: ORM) => Promise<T>): Promise<T> {
268
+ await this.adapter.beginTransaction();
269
+
270
+ try {
271
+ const result = await callback(this);
272
+ await this.adapter.commit();
273
+ return result;
274
+ } catch (error) {
275
+ await this.adapter.rollback();
276
+ throw error;
277
+ }
278
+ }
279
+
280
+ // ==================== Schema Methods ====================
281
+
282
+ /**
283
+ * Get all table names
284
+ */
285
+ async getTables(): Promise<string[]> {
286
+ if (this.adapter.getTables) {
287
+ return this.adapter.getTables();
288
+ }
289
+ throw new Error("Adapter does not support getTables()");
290
+ }
291
+
292
+ /**
293
+ * Get table schema
294
+ */
295
+ async getTableSchema(table: string): Promise<unknown[]> {
296
+ if (this.adapter.getTableSchema) {
297
+ return this.adapter.getTableSchema(table);
298
+ }
299
+ throw new Error("Adapter does not support getTableSchema()");
300
+ }
301
+
302
+ // ==================== Lifecycle ====================
303
+
304
+ /**
305
+ * Close connection
306
+ */
307
+ async close(): Promise<void> {
308
+ return this.adapter.close();
309
+ }
310
+
311
+ // ==================== Internal Methods ====================
312
+
313
+ /**
314
+ * Normalize query options
315
+ */
316
+ private normalizeOptions(options?: QueryOptions): QueryOptions {
317
+ return {
318
+ ...options,
319
+ lang: options?.lang || this.defaultLang,
320
+ fallbackLang: options?.fallbackLang || this.fallbackLang,
321
+ };
322
+ }
323
+
324
+ /**
325
+ * Validate data against schema
326
+ */
327
+ private validateData(
328
+ table: string,
329
+ data: Record<string, unknown>,
330
+ isUpdate = false
331
+ ): void {
332
+ if (!this.schema) return;
333
+
334
+ const tableDef = this.schema.tables[table];
335
+ if (!tableDef) {
336
+ throw new Error(`Table "${table}" not found in schema`);
337
+ }
338
+
339
+ // Skip validation for now - can be implemented with more detailed checks
340
+ // This is a placeholder for future validation logic
341
+ }
342
+
343
+ /**
344
+ * Resolve populate for records
345
+ */
346
+ private async resolvePopulateForRecords<T>(
347
+ records: T[],
348
+ table: string,
349
+ populate: QueryOptions["populate"]
350
+ ): Promise<T[]> {
351
+ if (!this.schema) return records;
352
+
353
+ // Validate populate options
354
+ const validation = validatePopulate(populate, table, this.schema);
355
+ if (!validation.valid) {
356
+ console.warn("Populate validation warnings:", validation.errors);
357
+ }
358
+
359
+ // Create adapter wrapper for populate resolver
360
+ const adapterWrapper = {
361
+ findMany: <R = unknown>(
362
+ t: string,
363
+ opts?: { where?: Record<string, unknown> }
364
+ ): Promise<R[]> => {
365
+ return this.adapter.list<R>(t, opts);
366
+ },
367
+ };
368
+
369
+ return resolvePopulate<T>(
370
+ records,
371
+ table,
372
+ populate,
373
+ this.schema,
374
+ adapterWrapper
375
+ );
376
+ }
377
+
378
+ /**
379
+ * Get the underlying adapter (for advanced usage)
380
+ */
381
+ getAdapter(): IDataAdapter {
382
+ return this.adapter;
383
+ }
384
+
385
+ /**
386
+ * Get current schema (if set)
387
+ */
388
+ getSchema(): SchemaDefinition | undefined {
389
+ return this.schema;
390
+ }
391
+
392
+ /**
393
+ * Set schema
394
+ */
395
+ setSchema(schema: SchemaDefinition): void {
396
+ this.schema = schema;
397
+ }
398
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Data Adapter Interface
3
+ *
4
+ * All data sources must implement this interface.
5
+ * This is the contract between ORM and adapters.
6
+ *
7
+ * Features:
8
+ * - Built-in multi-language support via lang option
9
+ * - Automatic translation handling for translatable fields
10
+ * - MongoDB-style populate for references
11
+ */
12
+
13
+ import type { QueryOptions, PaginatedResult, SchemaDefinition } from "../types";
14
+
15
+ /**
16
+ * Generic Data Adapter Interface
17
+ */
18
+ export interface IDataAdapter {
19
+ /**
20
+ * Schema definition for translation support (optional)
21
+ */
22
+ schema?: SchemaDefinition;
23
+
24
+ /**
25
+ * Default language for translations
26
+ */
27
+ defaultLang?: string;
28
+
29
+ /**
30
+ * Set schema after construction
31
+ */
32
+ setSchema?(schema: SchemaDefinition): void;
33
+
34
+ /**
35
+ * Initialize the adapter (optional)
36
+ */
37
+ connect?(): void | Promise<void>;
38
+
39
+ // ==================== Query Methods ====================
40
+
41
+ /**
42
+ * List records from table
43
+ */
44
+ list<T = unknown>(table: string, options?: QueryOptions): Promise<T[]>;
45
+
46
+ /**
47
+ * Get single record by ID
48
+ */
49
+ get<T = unknown>(
50
+ table: string,
51
+ id: string | number,
52
+ options?: QueryOptions
53
+ ): Promise<T | null>;
54
+
55
+ /**
56
+ * Count records matching condition
57
+ */
58
+ count(table: string, options?: QueryOptions): Promise<number>;
59
+
60
+ /**
61
+ * Query with pagination
62
+ */
63
+ paginate<T = unknown>(
64
+ table: string,
65
+ page: number,
66
+ limit: number,
67
+ options?: QueryOptions
68
+ ): Promise<PaginatedResult<T>>;
69
+
70
+ // ==================== Write Methods ====================
71
+
72
+ /**
73
+ * Create new record
74
+ */
75
+ create<T = unknown>(table: string, data: Record<string, unknown>): Promise<T>;
76
+
77
+ /**
78
+ * Update record by ID
79
+ */
80
+ update<T = unknown>(
81
+ table: string,
82
+ id: string | number,
83
+ data: Record<string, unknown>
84
+ ): Promise<T>;
85
+
86
+ /**
87
+ * Delete record by ID
88
+ */
89
+ delete(table: string, id: string | number): Promise<boolean>;
90
+
91
+ // ==================== Batch Methods ====================
92
+
93
+ /**
94
+ * Create multiple records
95
+ */
96
+ createMany<T = unknown>(
97
+ table: string,
98
+ records: Record<string, unknown>[],
99
+ options?: { ignore?: boolean }
100
+ ): Promise<{ created: number; ids: (number | bigint)[] }>;
101
+
102
+ /**
103
+ * Update multiple records
104
+ */
105
+ updateMany(
106
+ table: string,
107
+ updates: { id: number | string; data: Record<string, unknown> }[]
108
+ ): Promise<{ updated: number }>;
109
+
110
+ /**
111
+ * Delete multiple records
112
+ */
113
+ deleteMany(
114
+ table: string,
115
+ ids: (number | string)[]
116
+ ): Promise<{ deleted: number }>;
117
+
118
+ // ==================== Translation Methods ====================
119
+
120
+ /**
121
+ * Create record with translations in a single transaction
122
+ * If translations not provided but data contains translatable fields,
123
+ * those fields are automatically added as defaultLang translation
124
+ */
125
+ createWithTranslations<T = unknown>(
126
+ table: string,
127
+ data: Record<string, unknown>,
128
+ translations?: Record<string, Record<string, unknown>>
129
+ ): Promise<T>;
130
+
131
+ /**
132
+ * Update or insert translation for specific language
133
+ */
134
+ upsertTranslation(
135
+ table: string,
136
+ id: string | number,
137
+ lang: string,
138
+ data: Record<string, unknown>
139
+ ): Promise<void>;
140
+
141
+ /**
142
+ * Get all translations for a record
143
+ */
144
+ getTranslations<T = unknown>(table: string, id: string | number): Promise<T[]>;
145
+
146
+ // ==================== Raw Query Methods ====================
147
+
148
+ /**
149
+ * Execute raw query (adapter-specific)
150
+ */
151
+ raw<T = unknown>(query: string, params?: unknown[]): Promise<T[]>;
152
+
153
+ /**
154
+ * Execute raw statement (for INSERT/UPDATE/DELETE)
155
+ */
156
+ execute(
157
+ query: string,
158
+ params?: unknown[]
159
+ ): Promise<{ changes: number; lastInsertRowid: number | bigint }>;
160
+
161
+ // ==================== Transaction Methods ====================
162
+
163
+ /**
164
+ * Begin transaction
165
+ */
166
+ beginTransaction(): Promise<void>;
167
+
168
+ /**
169
+ * Commit transaction
170
+ */
171
+ commit(): Promise<void>;
172
+
173
+ /**
174
+ * Rollback transaction
175
+ */
176
+ rollback(): Promise<void>;
177
+
178
+ // ==================== Schema Methods ====================
179
+
180
+ /**
181
+ * Get all table names
182
+ */
183
+ getTables?(): Promise<string[]>;
184
+
185
+ /**
186
+ * Get table schema/structure
187
+ */
188
+ getTableSchema?(table: string): Promise<unknown[]>;
189
+
190
+ // ==================== Lifecycle ====================
191
+
192
+ /**
193
+ * Close connection
194
+ */
195
+ close(): void | Promise<void>;
196
+ }