@promakeai/orm 1.0.0 → 1.0.3

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @promakeai/orm
2
2
 
3
- Core ORM package with schema DSL, query builder, and multi-language support.
3
+ Core ORM package with schema DSL, query builder, and multi-language support. Platform-agnostic - works in both browser and Node.js.
4
4
 
5
5
  ## Installation
6
6
 
@@ -15,29 +15,36 @@ import { defineSchema, f } from '@promakeai/orm';
15
15
 
16
16
  const schema = defineSchema({
17
17
  name: 'myapp',
18
- languages: ['en', 'tr'],
19
- defaultLanguage: 'en',
18
+ languages: ['en', 'tr', 'de'],
20
19
  tables: {
21
20
  users: {
22
21
  id: f.id(),
23
- email: f.string().required().unique(),
24
- name: f.string().required(),
25
- role: f.string().default('user').enum(['user', 'admin']),
26
- createdAt: f.timestamp().default('now'),
22
+ email: f.string().required().unique().lowercase(),
23
+ name: f.string().required().trim(),
24
+ age: f.int().min(0).max(150),
25
+ role: f.string().enum(['user', 'admin']).default('user'),
26
+ bio: f.text().translatable(),
27
+ createdAt: f.timestamp(),
28
+ metadata: f.json(),
27
29
  },
28
30
  products: {
29
31
  id: f.id(),
30
32
  sku: f.string().required().unique(),
31
- price: f.decimal().required(),
33
+ price: f.decimal().required().min(0),
32
34
  stock: f.int().default(0),
33
- name: f.string().translatable(), // Multi-language
34
- description: f.text().translatable(), // Multi-language
35
- categoryId: f.int().ref('categories'), // Reference
35
+ name: f.string().translatable().required(),
36
+ description: f.text().translatable(),
37
+ categoryId: f.int().ref('categories'),
38
+ tagIds: f.json().ref('tags'), // Array reference
36
39
  },
37
40
  categories: {
38
41
  id: f.id(),
39
42
  slug: f.string().required().unique(),
40
- name: f.string().translatable(),
43
+ name: f.string().translatable().required(),
44
+ parentId: f.int().ref({
45
+ table: 'categories',
46
+ onDelete: 'SET_NULL',
47
+ }),
41
48
  },
42
49
  },
43
50
  });
@@ -45,109 +52,407 @@ const schema = defineSchema({
45
52
 
46
53
  ## Field Types
47
54
 
48
- | Type | Description |
49
- |------|-------------|
50
- | `f.id()` | Auto-increment primary key |
51
- | `f.string()` | Short text |
52
- | `f.text()` | Long text |
53
- | `f.int()` | Integer |
54
- | `f.decimal()` | Decimal number |
55
- | `f.bool()` | Boolean |
56
- | `f.timestamp()` | ISO datetime |
57
- | `f.json()` | JSON data |
55
+ | Type | SQL | Description |
56
+ |------|-----|-------------|
57
+ | `f.id()` | INTEGER PRIMARY KEY AUTOINCREMENT | Auto-increment primary key |
58
+ | `f.string()` | VARCHAR | Short text |
59
+ | `f.text()` | TEXT | Long text |
60
+ | `f.int()` | INTEGER | Integer |
61
+ | `f.decimal()` | REAL/DECIMAL | Decimal number |
62
+ | `f.bool()` | INTEGER (0/1) | Boolean |
63
+ | `f.timestamp()` | TEXT (ISO) | ISO datetime string |
64
+ | `f.json()` | TEXT | JSON serialized data |
58
65
 
59
66
  ## Field Modifiers
60
67
 
68
+ ### Constraints
69
+
61
70
  ```typescript
62
71
  f.string()
63
72
  .required() // NOT NULL
73
+ .nullable() // Allow NULL (default)
64
74
  .unique() // UNIQUE constraint
65
- .default('value') // Default value
75
+ .primary() // PRIMARY KEY
76
+ .default('value') // DEFAULT value
77
+ ```
78
+
79
+ ### String Transforms
80
+
81
+ ```typescript
82
+ f.string()
83
+ .trim() // Remove whitespace
84
+ .lowercase() // Convert to lowercase
85
+ .uppercase() // Convert to uppercase
86
+ ```
87
+
88
+ ### Validation
89
+
90
+ ```typescript
91
+ f.string()
92
+ .minLength(1) // Minimum length
93
+ .maxLength(255) // Maximum length
66
94
  .enum(['a', 'b', 'c']) // Allowed values
67
- .min(1).max(100) // Value bounds
68
- .translatable() // Multi-language field
69
- .ref('table') // Reference to another table
70
- .ref({ table: 'users', field: 'id', onDelete: 'CASCADE' })
95
+ .match(/^[a-z]+$/) // RegExp pattern
96
+
97
+ f.int()
98
+ .min(0) // Minimum value
99
+ .max(100) // Maximum value
100
+ ```
101
+
102
+ ### References
103
+
104
+ ```typescript
105
+ // Simple reference
106
+ f.int().ref('users')
107
+
108
+ // With options
109
+ f.int().ref({
110
+ table: 'users',
111
+ field: 'id', // Default: 'id'
112
+ onDelete: 'CASCADE', // CASCADE | SET_NULL | RESTRICT | NO_ACTION
113
+ onUpdate: 'CASCADE',
114
+ })
115
+
116
+ // Array reference (JSON field with refs)
117
+ f.json().ref('tags')
118
+ ```
119
+
120
+ ### Multi-Language
121
+
122
+ ```typescript
123
+ f.string().translatable() // Stored in {table}_translations
124
+ f.text().translatable()
71
125
  ```
72
126
 
73
127
  ## Query Builder (MongoDB-style)
74
128
 
75
129
  ```typescript
76
- import { buildWhere } from '@promakeai/orm';
130
+ import { buildWhereClause } from '@promakeai/orm';
77
131
 
78
132
  // Simple equality
79
- buildWhere({ status: 'active' });
80
- // WHERE status = 'active'
133
+ buildWhereClause({ status: 'active' });
134
+ // WHERE status = ? params: ['active']
81
135
 
82
136
  // Comparison operators
83
- buildWhere({ price: { $gt: 100 } }); // > 100
84
- buildWhere({ price: { $gte: 100 } }); // >= 100
85
- buildWhere({ price: { $lt: 100 } }); // < 100
86
- buildWhere({ price: { $lte: 100 } }); // <= 100
87
- buildWhere({ status: { $ne: 'deleted' } }); // != 'deleted'
137
+ buildWhereClause({ price: { $gt: 100 } }); // > 100
138
+ buildWhereClause({ price: { $gte: 100 } }); // >= 100
139
+ buildWhereClause({ price: { $lt: 100 } }); // < 100
140
+ buildWhereClause({ price: { $lte: 100 } }); // <= 100
141
+ buildWhereClause({ status: { $ne: 'deleted' } }); // != 'deleted'
88
142
 
89
143
  // Array operators
90
- buildWhere({ id: { $in: [1, 2, 3] } }); // IN (1, 2, 3)
91
- buildWhere({ id: { $nin: [1, 2, 3] } }); // NOT IN (1, 2, 3)
144
+ buildWhereClause({ id: { $in: [1, 2, 3] } }); // IN (?, ?, ?)
145
+ buildWhereClause({ id: { $nin: [1, 2, 3] } }); // NOT IN (?, ?, ?)
92
146
 
93
147
  // String matching
94
- buildWhere({ name: { $like: '%john%' } }); // LIKE '%john%'
95
- buildWhere({ name: { $notLike: '%test%' } }); // NOT LIKE '%test%'
148
+ buildWhereClause({ name: { $like: '%john%' } }); // LIKE ?
149
+ buildWhereClause({ name: { $notLike: '%test%' } }); // NOT LIKE ?
96
150
 
97
151
  // Range and null
98
- buildWhere({ price: { $between: [10, 100] } }); // BETWEEN 10 AND 100
99
- buildWhere({ deletedAt: { $isNull: true } }); // IS NULL
100
- buildWhere({ email: { $isNull: false } }); // IS NOT NULL
152
+ buildWhereClause({ price: { $between: [10, 100] } }); // BETWEEN ? AND ?
153
+ buildWhereClause({ deletedAt: { $isNull: true } }); // IS NULL
154
+ buildWhereClause({ email: { $isNull: false } }); // IS NOT NULL
101
155
 
102
156
  // Logical operators
103
- buildWhere({ $or: [{ active: true }, { role: 'admin' }] });
104
- buildWhere({ $and: [{ price: { $gt: 50 } }, { stock: { $gt: 0 } }] });
105
- buildWhere({ $nor: [{ banned: true }, { suspended: true }] });
157
+ buildWhereClause({
158
+ $or: [
159
+ { active: true },
160
+ { role: 'admin' }
161
+ ]
162
+ });
163
+ // (active = ? OR role = ?)
164
+
165
+ buildWhereClause({
166
+ $and: [
167
+ { price: { $gt: 50 } },
168
+ { stock: { $gt: 0 } }
169
+ ]
170
+ });
171
+ // (price > ? AND stock > ?)
172
+
173
+ buildWhereClause({
174
+ $nor: [
175
+ { banned: true },
176
+ { suspended: true }
177
+ ]
178
+ });
179
+ // NOT (banned = ? OR suspended = ?)
180
+
181
+ // Negation
182
+ buildWhereClause({
183
+ age: { $not: { $lt: 18 } }
184
+ });
185
+ // NOT (age < ?)
186
+
187
+ // Combined
188
+ buildWhereClause({
189
+ status: 'active',
190
+ price: { $gt: 100, $lt: 500 },
191
+ category: { $in: ['electronics', 'books'] },
192
+ });
193
+ // status = ? AND price > ? AND price < ? AND category IN (?, ?)
106
194
  ```
107
195
 
108
196
  ## Multi-Language Support
109
197
 
110
- Schema fields marked with `.translatable()` are stored in separate translation tables:
198
+ ### Schema Configuration
111
199
 
112
200
  ```typescript
113
- // Schema
114
- products: {
115
- id: f.id(),
116
- price: f.decimal(),
117
- name: f.string().translatable(), // Stored in products_translations
118
- description: f.text().translatable(), // Stored in products_translations
119
- }
201
+ const schema = defineSchema({
202
+ languages: ['en', 'tr', 'de'], // Supported languages
203
+ // OR
204
+ languages: {
205
+ default: 'en',
206
+ supported: ['en', 'tr', 'de'],
207
+ },
208
+ tables: {
209
+ products: {
210
+ id: f.id(),
211
+ price: f.decimal(),
212
+ name: f.string().translatable(), // In translation table
213
+ description: f.text().translatable(), // In translation table
214
+ },
215
+ },
216
+ });
217
+ ```
218
+
219
+ ### Generated Tables
220
+
221
+ ```sql
222
+ -- Main table
223
+ CREATE TABLE products (
224
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
225
+ price REAL
226
+ );
120
227
 
121
- // Creates tables:
122
- // - products (id, price)
123
- // - products_translations (id, product_id, lang, name, description)
228
+ -- Translation table
229
+ CREATE TABLE products_translations (
230
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
231
+ product_id INTEGER NOT NULL,
232
+ lang TEXT NOT NULL,
233
+ name TEXT,
234
+ description TEXT,
235
+ FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE,
236
+ UNIQUE (product_id, lang)
237
+ );
124
238
  ```
125
239
 
126
- Query with language:
240
+ ### Translation Query Builder
127
241
 
128
242
  ```typescript
129
- const products = await dm.query('products', {
243
+ import { buildTranslationQuery } from '@promakeai/orm';
244
+
245
+ const { sql, params } = buildTranslationQuery('products', schema, {
130
246
  lang: 'tr',
131
- fallbackLang: 'en', // Falls back if translation missing
247
+ fallbackLang: 'en',
248
+ where: { price: { $gt: 100 } },
249
+ orderBy: [{ field: 'name', direction: 'ASC' }],
250
+ limit: 10,
132
251
  });
252
+
253
+ // SELECT
254
+ // m.id, m.price,
255
+ // COALESCE(t.name, fb.name) AS name,
256
+ // COALESCE(t.description, fb.description) AS description
257
+ // FROM products m
258
+ // LEFT JOIN products_translations t ON m.id = t.product_id AND t.lang = ?
259
+ // LEFT JOIN products_translations fb ON m.id = fb.product_id AND fb.lang = ?
260
+ // WHERE price > ?
261
+ // ORDER BY name ASC
262
+ // LIMIT 10
263
+ ```
264
+
265
+ ## Populate Resolver
266
+
267
+ Batch-fetches references to prevent N+1 queries:
268
+
269
+ ```typescript
270
+ import { resolvePopulate } from '@promakeai/orm';
271
+
272
+ const posts = await adapter.list('posts');
273
+
274
+ // Resolve author references
275
+ const postsWithAuthors = await resolvePopulate(
276
+ posts,
277
+ { userId: true },
278
+ schema,
279
+ adapter
280
+ );
281
+ // [{ id: 1, title: '...', userId: { id: 1, name: 'John' } }]
282
+
283
+ // Nested populate
284
+ const postsWithDetails = await resolvePopulate(
285
+ posts,
286
+ {
287
+ userId: true,
288
+ comments: {
289
+ populate: { authorId: true },
290
+ where: { approved: true },
291
+ limit: 5,
292
+ },
293
+ },
294
+ schema,
295
+ adapter
296
+ );
297
+
298
+ // Array reference populate
299
+ const postsWithTags = await resolvePopulate(
300
+ posts,
301
+ { tagIds: true }, // JSON array of tag IDs
302
+ schema,
303
+ adapter
304
+ );
305
+ ```
306
+
307
+ ## Schema Helpers
308
+
309
+ ```typescript
310
+ import {
311
+ getTranslatableFields,
312
+ getNonTranslatableFields,
313
+ getReferenceFields,
314
+ getPrimaryKeyField,
315
+ getRequiredFields,
316
+ isRequiredField,
317
+ getMainTableFields,
318
+ getTranslationTableFields,
319
+ toTranslationTableName,
320
+ toTranslationFKName,
321
+ singularize,
322
+ pluralize,
323
+ toPascalCase,
324
+ toCamelCase,
325
+ toSnakeCase,
326
+ } from '@promakeai/orm';
327
+
328
+ const table = schema.tables.products;
329
+
330
+ getTranslatableFields(table); // ['name', 'description']
331
+ getNonTranslatableFields(table); // ['id', 'price', 'sku', 'stock', 'categoryId']
332
+ getReferenceFields(table); // [['categoryId', { table: 'categories', ... }]]
333
+ getPrimaryKeyField(table); // 'id'
334
+ getRequiredFields(table); // ['sku', 'price', 'name']
335
+
336
+ toTranslationTableName('products'); // 'products_translations'
337
+ toTranslationFKName('products'); // 'product_id'
338
+
339
+ singularize('products'); // 'product'
340
+ pluralize('product'); // 'products'
341
+ toPascalCase('user_id'); // 'UserId'
342
+ toCamelCase('user_id'); // 'userId'
343
+ toSnakeCase('userId'); // 'user_id'
344
+ ```
345
+
346
+ ## Schema Validation
347
+
348
+ ```typescript
349
+ import {
350
+ validateSchema,
351
+ assertValidSchema,
352
+ isValidSchema,
353
+ } from '@promakeai/orm';
354
+
355
+ // Returns array of errors
356
+ const errors = validateSchema(schema);
357
+ // [{ path: 'tables.users.email', message: '...' }]
358
+
359
+ // Throws on invalid schema
360
+ assertValidSchema(schema);
361
+
362
+ // Boolean check
363
+ if (isValidSchema(schema)) {
364
+ // ...
365
+ }
366
+ ```
367
+
368
+ ## Schema Merging
369
+
370
+ ```typescript
371
+ import { mergeSchemas } from '@promakeai/orm';
372
+
373
+ const schema1 = defineSchema({
374
+ tables: { users: { ... } },
375
+ });
376
+
377
+ const schema2 = defineSchema({
378
+ tables: { products: { ... } },
379
+ });
380
+
381
+ const merged = mergeSchemas([schema1, schema2]);
382
+ // { tables: { users: {...}, products: {...} } }
383
+ ```
384
+
385
+ ## IDataAdapter Interface
386
+
387
+ All adapters must implement this interface:
388
+
389
+ ```typescript
390
+ interface IDataAdapter {
391
+ schema?: SchemaDefinition;
392
+ defaultLang?: string;
393
+
394
+ // Query methods
395
+ list<T>(table: string, options?: QueryOptions): Promise<T[]>;
396
+ get<T>(table: string, id: string | number, options?: QueryOptions): Promise<T | null>;
397
+ count(table: string, options?: QueryOptions): Promise<number>;
398
+ paginate<T>(table: string, page: number, limit: number, options?: QueryOptions): Promise<PaginatedResult<T>>;
399
+
400
+ // Write methods
401
+ create<T>(table: string, data: Record<string, unknown>): Promise<T>;
402
+ update<T>(table: string, id: string | number, data: Record<string, unknown>): Promise<T>;
403
+ delete(table: string, id: string | number): Promise<boolean>;
404
+
405
+ // Batch methods
406
+ createMany<T>(table: string, records: Record<string, unknown>[], options?: { ignore?: boolean }): Promise<{ created: number; ids: (number | bigint)[] }>;
407
+ updateMany(table: string, updates: { id: number | string; data: Record<string, unknown> }[]): Promise<{ updated: number }>;
408
+ deleteMany(table: string, ids: (number | string)[]): Promise<{ deleted: number }>;
409
+
410
+ // Translation methods
411
+ createWithTranslations<T>(table: string, data: Record<string, unknown>, translations?: Record<string, Record<string, unknown>>): Promise<T>;
412
+ upsertTranslation(table: string, id: string | number, lang: string, data: Record<string, unknown>): Promise<void>;
413
+ getTranslations<T>(table: string, id: string | number): Promise<T[]>;
414
+
415
+ // Raw queries
416
+ raw<T>(query: string, params?: unknown[]): Promise<T[]>;
417
+ execute(query: string, params?: unknown[]): Promise<{ changes: number; lastInsertRowid: number | bigint }>;
418
+
419
+ // Transactions
420
+ beginTransaction(): Promise<void>;
421
+ commit(): Promise<void>;
422
+ rollback(): Promise<void>;
423
+
424
+ // Schema
425
+ getTables?(): Promise<string[]>;
426
+ getTableSchema?(table: string): Promise<unknown[]>;
427
+
428
+ // Lifecycle
429
+ connect?(): void | Promise<void>;
430
+ close(): void | Promise<void>;
431
+ }
133
432
  ```
134
433
 
135
- ## Types
434
+ ## TypeScript Types
136
435
 
137
436
  ```typescript
138
437
  import type {
139
- Schema,
438
+ SchemaDefinition,
140
439
  TableDefinition,
141
440
  FieldDefinition,
441
+ FieldType,
442
+ FieldReference,
142
443
  QueryOptions,
143
444
  WhereClause,
445
+ OrderByOption,
446
+ PopulateOption,
447
+ PaginatedResult,
448
+ IDataAdapter,
144
449
  } from '@promakeai/orm';
145
450
  ```
146
451
 
147
452
  ## Related Packages
148
453
 
149
- - [@promakeai/dbcli](../dbcli) - CLI tool
150
- - [@promakeai/dbreact](../dbreact) - React integration
454
+ - [@promakeai/dbcli](../dbcli) - CLI tool for database operations
455
+ - [@promakeai/dbreact](../dbreact) - React hooks and providers
151
456
 
152
457
  ## License
153
458
 
@@ -38,6 +38,10 @@ export interface IDataAdapter {
38
38
  * Get single record by ID
39
39
  */
40
40
  get<T = unknown>(table: string, id: string | number, options?: QueryOptions): Promise<T | null>;
41
+ /**
42
+ * Find first record matching query (like Mongoose findOne)
43
+ */
44
+ findOne<T = unknown>(table: string, options?: QueryOptions): Promise<T | null>;
41
45
  /**
42
46
  * Count records matching condition
43
47
  */
package/dist/index.d.ts CHANGED
@@ -47,4 +47,6 @@ export type { TranslationQueryOptions, TranslationQueryResult, } from "./utils/t
47
47
  export { resolvePopulate, getPopulatableFields, validatePopulate, } from "./utils/populateResolver";
48
48
  export type { PopulateAdapter } from "./utils/populateResolver";
49
49
  export { parseJSONSchema } from "./utils/jsonConverter";
50
- export type { FieldType, FieldDefinition, FieldReference, FieldBuilderLike, TableDefinition, LanguageConfig, SchemaDefinition, SchemaInput, JSONFieldDefinition, JSONTableDefinition, JSONSchemaDefinition, WhereCondition, OrderByOption, PopulateOption, PopulateNested, QueryOptions, PaginatedResult, ORMConfig, } from "./types";
50
+ export { isJsonType } from "./types";
51
+ export { deserializeRow, serializeRow } from "./utils/deserializer";
52
+ export type { FieldType, FieldDefinition, FieldReference, FieldBuilderLike, TableDefinition, LanguageConfig, SchemaDefinition, SchemaInput, JSONFieldType, JSONFieldDefinition, JSONTableDefinition, JSONSchemaDefinition, WhereCondition, OrderByOption, PopulateOption, PopulateNested, QueryOptions, PaginatedResult, ORMConfig, } from "./types";