@promakeai/orm 1.0.0 → 1.0.1
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 +368 -63
- package/dist/adapters/IDataAdapter.d.ts +4 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +47 -0
- package/dist/utils/deserializer.d.ts +24 -0
- package/package.json +3 -2
- package/src/adapters/IDataAdapter.ts +5 -0
- package/src/index.ts +3 -0
- package/src/utils/deserializer.ts +91 -0
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
|
-
|
|
26
|
-
|
|
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(),
|
|
34
|
-
description: f.text().translatable(),
|
|
35
|
-
categoryId: f.int().ref('categories'),
|
|
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
|
-
.
|
|
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
|
-
.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
.
|
|
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 {
|
|
130
|
+
import { buildWhereClause } from '@promakeai/orm';
|
|
77
131
|
|
|
78
132
|
// Simple equality
|
|
79
|
-
|
|
80
|
-
// WHERE status = 'active'
|
|
133
|
+
buildWhereClause({ status: 'active' });
|
|
134
|
+
// WHERE status = ? params: ['active']
|
|
81
135
|
|
|
82
136
|
// Comparison operators
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
91
|
-
|
|
144
|
+
buildWhereClause({ id: { $in: [1, 2, 3] } }); // IN (?, ?, ?)
|
|
145
|
+
buildWhereClause({ id: { $nin: [1, 2, 3] } }); // NOT IN (?, ?, ?)
|
|
92
146
|
|
|
93
147
|
// String matching
|
|
94
|
-
|
|
95
|
-
|
|
148
|
+
buildWhereClause({ name: { $like: '%john%' } }); // LIKE ?
|
|
149
|
+
buildWhereClause({ name: { $notLike: '%test%' } }); // NOT LIKE ?
|
|
96
150
|
|
|
97
151
|
// Range and null
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
198
|
+
### Schema Configuration
|
|
111
199
|
|
|
112
200
|
```typescript
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
240
|
+
### Translation Query Builder
|
|
127
241
|
|
|
128
242
|
```typescript
|
|
129
|
-
|
|
243
|
+
import { buildTranslationQuery } from '@promakeai/orm';
|
|
244
|
+
|
|
245
|
+
const { sql, params } = buildTranslationQuery('products', schema, {
|
|
130
246
|
lang: 'tr',
|
|
131
|
-
fallbackLang: 'en',
|
|
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
|
-
|
|
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
|
|
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,5 @@ 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 { deserializeRow, serializeRow } from "./utils/deserializer";
|
|
50
51
|
export type { FieldType, FieldDefinition, FieldReference, FieldBuilderLike, TableDefinition, LanguageConfig, SchemaDefinition, SchemaInput, JSONFieldDefinition, JSONTableDefinition, JSONSchemaDefinition, WhereCondition, OrderByOption, PopulateOption, PopulateNested, QueryOptions, PaginatedResult, ORMConfig, } from "./types";
|
package/dist/index.js
CHANGED
|
@@ -1008,6 +1008,51 @@ function convertJSONField(jsonField) {
|
|
|
1008
1008
|
ref
|
|
1009
1009
|
};
|
|
1010
1010
|
}
|
|
1011
|
+
// src/utils/deserializer.ts
|
|
1012
|
+
function deserializeValue(value, fieldType) {
|
|
1013
|
+
if (value === null || value === undefined)
|
|
1014
|
+
return value;
|
|
1015
|
+
if (fieldType === "bool") {
|
|
1016
|
+
return value === 1 || value === true;
|
|
1017
|
+
}
|
|
1018
|
+
if (fieldType === "json" && typeof value === "string") {
|
|
1019
|
+
try {
|
|
1020
|
+
return JSON.parse(value);
|
|
1021
|
+
} catch {
|
|
1022
|
+
return value;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return value;
|
|
1026
|
+
}
|
|
1027
|
+
function deserializeRow(row, fields) {
|
|
1028
|
+
const result = { ...row };
|
|
1029
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
1030
|
+
if (fieldName in result) {
|
|
1031
|
+
result[fieldName] = deserializeValue(result[fieldName], field.type);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return result;
|
|
1035
|
+
}
|
|
1036
|
+
function serializeValue(value, fieldType) {
|
|
1037
|
+
if (value === null || value === undefined)
|
|
1038
|
+
return value;
|
|
1039
|
+
if (fieldType === "bool") {
|
|
1040
|
+
return value ? 1 : 0;
|
|
1041
|
+
}
|
|
1042
|
+
if (fieldType === "json" && typeof value !== "string") {
|
|
1043
|
+
return JSON.stringify(value);
|
|
1044
|
+
}
|
|
1045
|
+
return value;
|
|
1046
|
+
}
|
|
1047
|
+
function serializeRow(data, fields) {
|
|
1048
|
+
const result = { ...data };
|
|
1049
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
1050
|
+
if (fieldName in result) {
|
|
1051
|
+
result[fieldName] = serializeValue(result[fieldName], field.type);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
return result;
|
|
1055
|
+
}
|
|
1011
1056
|
export {
|
|
1012
1057
|
validateTable,
|
|
1013
1058
|
validateSchema,
|
|
@@ -1021,6 +1066,7 @@ export {
|
|
|
1021
1066
|
toDbInterfaceName,
|
|
1022
1067
|
toCamelCase,
|
|
1023
1068
|
singularize,
|
|
1069
|
+
serializeRow,
|
|
1024
1070
|
resolvePopulate,
|
|
1025
1071
|
pluralize,
|
|
1026
1072
|
parseJSONSchema,
|
|
@@ -1041,6 +1087,7 @@ export {
|
|
|
1041
1087
|
getInsertableFields,
|
|
1042
1088
|
f,
|
|
1043
1089
|
extractTranslatableData,
|
|
1090
|
+
deserializeRow,
|
|
1044
1091
|
defineSchema,
|
|
1045
1092
|
createSchemaUnsafe,
|
|
1046
1093
|
buildWhereClause,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Row Serializer/Deserializer
|
|
3
|
+
*
|
|
4
|
+
* Auto-converts between SQLite storage types and application types:
|
|
5
|
+
* - bool: 0/1 (SQLite) ↔ true/false (app)
|
|
6
|
+
* - json: TEXT (SQLite) ↔ parsed object/array (app)
|
|
7
|
+
*/
|
|
8
|
+
import type { FieldDefinition } from "../types";
|
|
9
|
+
/**
|
|
10
|
+
* Deserialize a database row using schema field definitions
|
|
11
|
+
*
|
|
12
|
+
* Converts SQLite storage types to application types:
|
|
13
|
+
* - bool fields: 0/1 → true/false
|
|
14
|
+
* - json fields: TEXT → parsed object/array
|
|
15
|
+
*/
|
|
16
|
+
export declare function deserializeRow<T = Record<string, unknown>>(row: Record<string, unknown>, fields: Record<string, FieldDefinition>): T;
|
|
17
|
+
/**
|
|
18
|
+
* Serialize application data for database storage
|
|
19
|
+
*
|
|
20
|
+
* Converts application types to SQLite storage types:
|
|
21
|
+
* - bool fields: true/false → 1/0
|
|
22
|
+
* - json fields: object/array → JSON string
|
|
23
|
+
*/
|
|
24
|
+
export declare function serializeRow(data: Record<string, unknown>, fields: Record<string, FieldDefinition>): Record<string, unknown>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promakeai/orm",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Database-agnostic ORM core - works in browser and Node.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"build": "bun build src/index.ts --outdir dist --target browser --format esm && bun x tsc --emitDeclarationOnly",
|
|
22
22
|
"dev": "bun build src/index.ts --outdir dist --target browser --format esm --watch",
|
|
23
23
|
"test": "bun test",
|
|
24
|
-
"typecheck": "bun x tsc --noEmit"
|
|
24
|
+
"typecheck": "bun x tsc --noEmit",
|
|
25
|
+
"release": "bun run build && npm publish --access public"
|
|
25
26
|
},
|
|
26
27
|
"files": [
|
|
27
28
|
"dist",
|
|
@@ -52,6 +52,11 @@ export interface IDataAdapter {
|
|
|
52
52
|
options?: QueryOptions
|
|
53
53
|
): Promise<T | null>;
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Find first record matching query (like Mongoose findOne)
|
|
57
|
+
*/
|
|
58
|
+
findOne<T = unknown>(table: string, options?: QueryOptions): Promise<T | null>;
|
|
59
|
+
|
|
55
60
|
/**
|
|
56
61
|
* Count records matching condition
|
|
57
62
|
*/
|
package/src/index.ts
CHANGED
|
@@ -116,6 +116,9 @@ export type { PopulateAdapter } from "./utils/populateResolver";
|
|
|
116
116
|
// JSON Schema Converter
|
|
117
117
|
export { parseJSONSchema } from "./utils/jsonConverter";
|
|
118
118
|
|
|
119
|
+
// Row Serializer/Deserializer
|
|
120
|
+
export { deserializeRow, serializeRow } from "./utils/deserializer";
|
|
121
|
+
|
|
119
122
|
// Types
|
|
120
123
|
export type {
|
|
121
124
|
// Field types
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Row Serializer/Deserializer
|
|
3
|
+
*
|
|
4
|
+
* Auto-converts between SQLite storage types and application types:
|
|
5
|
+
* - bool: 0/1 (SQLite) ↔ true/false (app)
|
|
6
|
+
* - json: TEXT (SQLite) ↔ parsed object/array (app)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { FieldType, FieldDefinition } from "../types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Deserialize a single value from DB storage type to application type
|
|
13
|
+
*/
|
|
14
|
+
function deserializeValue(value: unknown, fieldType: FieldType): unknown {
|
|
15
|
+
if (value === null || value === undefined) return value;
|
|
16
|
+
|
|
17
|
+
if (fieldType === "bool") {
|
|
18
|
+
return value === 1 || value === true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (fieldType === "json" && typeof value === "string") {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(value);
|
|
24
|
+
} catch {
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Deserialize a database row using schema field definitions
|
|
34
|
+
*
|
|
35
|
+
* Converts SQLite storage types to application types:
|
|
36
|
+
* - bool fields: 0/1 → true/false
|
|
37
|
+
* - json fields: TEXT → parsed object/array
|
|
38
|
+
*/
|
|
39
|
+
export function deserializeRow<T = Record<string, unknown>>(
|
|
40
|
+
row: Record<string, unknown>,
|
|
41
|
+
fields: Record<string, FieldDefinition>
|
|
42
|
+
): T {
|
|
43
|
+
const result: Record<string, unknown> = { ...row };
|
|
44
|
+
|
|
45
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
46
|
+
if (fieldName in result) {
|
|
47
|
+
result[fieldName] = deserializeValue(result[fieldName], field.type);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return result as T;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Serialize a single value from application type to DB storage type
|
|
56
|
+
*/
|
|
57
|
+
function serializeValue(value: unknown, fieldType: FieldType): unknown {
|
|
58
|
+
if (value === null || value === undefined) return value;
|
|
59
|
+
|
|
60
|
+
if (fieldType === "bool") {
|
|
61
|
+
return value ? 1 : 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (fieldType === "json" && typeof value !== "string") {
|
|
65
|
+
return JSON.stringify(value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Serialize application data for database storage
|
|
73
|
+
*
|
|
74
|
+
* Converts application types to SQLite storage types:
|
|
75
|
+
* - bool fields: true/false → 1/0
|
|
76
|
+
* - json fields: object/array → JSON string
|
|
77
|
+
*/
|
|
78
|
+
export function serializeRow(
|
|
79
|
+
data: Record<string, unknown>,
|
|
80
|
+
fields: Record<string, FieldDefinition>
|
|
81
|
+
): Record<string, unknown> {
|
|
82
|
+
const result: Record<string, unknown> = { ...data };
|
|
83
|
+
|
|
84
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
85
|
+
if (fieldName in result) {
|
|
86
|
+
result[fieldName] = serializeValue(result[fieldName], field.type);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return result;
|
|
91
|
+
}
|