@promakeai/orm 1.0.3 → 1.0.5
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 +56 -25
- package/dist/index.js +9 -2
- package/dist/utils/translationQuery.d.ts +6 -1
- package/package.json +1 -1
- package/src/utils/translationQuery.ts +62 -44
package/README.md
CHANGED
|
@@ -13,9 +13,9 @@ npm install @promakeai/orm
|
|
|
13
13
|
```typescript
|
|
14
14
|
import { defineSchema, f } from '@promakeai/orm';
|
|
15
15
|
|
|
16
|
-
const schema = defineSchema({
|
|
17
|
-
name: 'myapp',
|
|
18
|
-
languages: ['en', 'tr', 'de'],
|
|
16
|
+
const schema = defineSchema({
|
|
17
|
+
name: 'myapp',
|
|
18
|
+
languages: ['en', 'tr', 'de'],
|
|
19
19
|
tables: {
|
|
20
20
|
users: {
|
|
21
21
|
id: f.id(),
|
|
@@ -47,21 +47,45 @@ const schema = defineSchema({
|
|
|
47
47
|
}),
|
|
48
48
|
},
|
|
49
49
|
},
|
|
50
|
-
});
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
##
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## JSON Schema (AI-Friendly)
|
|
54
|
+
|
|
55
|
+
The JSON schema format is the runtime-friendly version and supports native
|
|
56
|
+
arrays and typed objects:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"name": "myapp",
|
|
61
|
+
"languages": ["en", "tr"],
|
|
62
|
+
"defaultLanguage": "en",
|
|
63
|
+
"tables": {
|
|
64
|
+
"products": {
|
|
65
|
+
"id": { "type": "id" },
|
|
66
|
+
"tags": { "type": ["string"] },
|
|
67
|
+
"metadata": { "type": { "color": "string", "weight": "number" } },
|
|
68
|
+
"variants": { "type": [{ "sku": "string", "price": "number" }] }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Field Types
|
|
75
|
+
|
|
76
|
+
| Type | SQL | Description |
|
|
77
|
+
|------|-----|-------------|
|
|
78
|
+
| `f.id()` | INTEGER PRIMARY KEY AUTOINCREMENT | Auto-increment primary key |
|
|
79
|
+
| `f.string()` | VARCHAR | Short text |
|
|
80
|
+
| `f.text()` | TEXT | Long text |
|
|
81
|
+
| `f.int()` | INTEGER | Integer |
|
|
82
|
+
| `f.decimal()` | REAL/DECIMAL | Decimal number |
|
|
83
|
+
| `f.bool()` | INTEGER (0/1) | Boolean |
|
|
84
|
+
| `f.timestamp()` | TEXT (ISO) | ISO datetime string |
|
|
85
|
+
| `f.json()` | TEXT | JSON serialized data |
|
|
86
|
+
|
|
87
|
+
JSON schema type syntax supports `string[]`, `number[]`, `boolean[]`, `object`,
|
|
88
|
+
and `object[]` which are stored as JSON in SQL (TEXT) and typed in TS.
|
|
65
89
|
|
|
66
90
|
## Field Modifiers
|
|
67
91
|
|
|
@@ -148,10 +172,14 @@ buildWhereClause({ id: { $nin: [1, 2, 3] } }); // NOT IN (?, ?, ?)
|
|
|
148
172
|
buildWhereClause({ name: { $like: '%john%' } }); // LIKE ?
|
|
149
173
|
buildWhereClause({ name: { $notLike: '%test%' } }); // NOT LIKE ?
|
|
150
174
|
|
|
151
|
-
// Range and null
|
|
152
|
-
buildWhereClause({ price: { $between: [10, 100] } }); // BETWEEN ? AND ?
|
|
153
|
-
buildWhereClause({ deletedAt: { $isNull: true } }); // IS NULL
|
|
154
|
-
buildWhereClause({ email: { $isNull: false } }); // IS NOT NULL
|
|
175
|
+
// Range and null
|
|
176
|
+
buildWhereClause({ price: { $between: [10, 100] } }); // BETWEEN ? AND ?
|
|
177
|
+
buildWhereClause({ deletedAt: { $isNull: true } }); // IS NULL
|
|
178
|
+
buildWhereClause({ email: { $isNull: false } }); // IS NOT NULL
|
|
179
|
+
|
|
180
|
+
// JSON array contains
|
|
181
|
+
buildWhereClause({ tags: { $contains: "sale" } }); // json_each(...) = "sale"
|
|
182
|
+
buildWhereClause({ tags: { $containsAny: ["sale", "new"] } }); // json_each(...) IN (...)
|
|
155
183
|
|
|
156
184
|
// Logical operators
|
|
157
185
|
buildWhereClause({
|
|
@@ -262,9 +290,12 @@ const { sql, params } = buildTranslationQuery('products', schema, {
|
|
|
262
290
|
// LIMIT 10
|
|
263
291
|
```
|
|
264
292
|
|
|
265
|
-
## Populate Resolver
|
|
266
|
-
|
|
267
|
-
Batch-fetches references to prevent N+1 queries:
|
|
293
|
+
## Populate Resolver
|
|
294
|
+
|
|
295
|
+
Batch-fetches references to prevent N+1 queries:
|
|
296
|
+
|
|
297
|
+
Populate options accept a space-separated string, string array, or nested object
|
|
298
|
+
for deeper population.
|
|
268
299
|
|
|
269
300
|
```typescript
|
|
270
301
|
import { resolvePopulate } from '@promakeai/orm';
|
package/dist/index.js
CHANGED
|
@@ -852,6 +852,7 @@ function buildTranslationQuery(options) {
|
|
|
852
852
|
schema,
|
|
853
853
|
lang,
|
|
854
854
|
fallbackLang = schema.languages.default,
|
|
855
|
+
mainFallbackFields,
|
|
855
856
|
where,
|
|
856
857
|
orderBy,
|
|
857
858
|
limit,
|
|
@@ -870,10 +871,15 @@ function buildTranslationQuery(options) {
|
|
|
870
871
|
const mainAlias = "m";
|
|
871
872
|
const transAlias = "t";
|
|
872
873
|
const fallbackAlias = "fb";
|
|
874
|
+
const mainFallbackSet = mainFallbackFields !== undefined ? new Set(mainFallbackFields) : null;
|
|
873
875
|
const selectFields = [];
|
|
874
876
|
for (const [fieldName, field] of Object.entries(tableSchema.fields)) {
|
|
875
877
|
if (field.translatable) {
|
|
876
|
-
|
|
878
|
+
if (mainFallbackSet && !mainFallbackSet.has(fieldName)) {
|
|
879
|
+
selectFields.push(`COALESCE(${transAlias}.${fieldName}, ${fallbackAlias}.${fieldName}) as ${fieldName}`);
|
|
880
|
+
} else {
|
|
881
|
+
selectFields.push(`COALESCE(${transAlias}.${fieldName}, ${fallbackAlias}.${fieldName}, ${mainAlias}.${fieldName}) as ${fieldName}`);
|
|
882
|
+
}
|
|
877
883
|
} else {
|
|
878
884
|
selectFields.push(`${mainAlias}.${fieldName}`);
|
|
879
885
|
}
|
|
@@ -915,12 +921,13 @@ OFFSET ?`;
|
|
|
915
921
|
}
|
|
916
922
|
return { sql, params };
|
|
917
923
|
}
|
|
918
|
-
function buildTranslationQueryById(table, schema, id, lang, fallbackLang) {
|
|
924
|
+
function buildTranslationQueryById(table, schema, id, lang, fallbackLang, mainFallbackFields) {
|
|
919
925
|
return buildTranslationQuery({
|
|
920
926
|
table,
|
|
921
927
|
schema,
|
|
922
928
|
lang,
|
|
923
929
|
fallbackLang,
|
|
930
|
+
mainFallbackFields,
|
|
924
931
|
where: { id },
|
|
925
932
|
limit: 1
|
|
926
933
|
});
|
|
@@ -13,6 +13,11 @@ export interface TranslationQueryOptions {
|
|
|
13
13
|
schema: SchemaDefinition;
|
|
14
14
|
lang: string;
|
|
15
15
|
fallbackLang?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Main-table fields that can be safely used as cache fallback in COALESCE.
|
|
18
|
+
* If omitted, all translatable fields are assumed available in main table.
|
|
19
|
+
*/
|
|
20
|
+
mainFallbackFields?: string[];
|
|
16
21
|
where?: Record<string, unknown>;
|
|
17
22
|
orderBy?: OrderByOption[];
|
|
18
23
|
limit?: number;
|
|
@@ -32,7 +37,7 @@ export declare function buildTranslationQuery(options: TranslationQueryOptions):
|
|
|
32
37
|
/**
|
|
33
38
|
* Build query for single record by ID with translations
|
|
34
39
|
*/
|
|
35
|
-
export declare function buildTranslationQueryById(table: string, schema: SchemaDefinition, id: number | string, lang: string, fallbackLang?: string): TranslationQueryResult;
|
|
40
|
+
export declare function buildTranslationQueryById(table: string, schema: SchemaDefinition, id: number | string, lang: string, fallbackLang?: string, mainFallbackFields?: string[]): TranslationQueryResult;
|
|
36
41
|
/**
|
|
37
42
|
* Build INSERT statement for translation
|
|
38
43
|
*/
|
package/package.json
CHANGED
|
@@ -44,16 +44,21 @@ function buildAliasedWhereClause(
|
|
|
44
44
|
/**
|
|
45
45
|
* Options for translation queries
|
|
46
46
|
*/
|
|
47
|
-
export interface TranslationQueryOptions {
|
|
48
|
-
table: string;
|
|
49
|
-
schema: SchemaDefinition;
|
|
50
|
-
lang: string;
|
|
51
|
-
fallbackLang?: string;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
47
|
+
export interface TranslationQueryOptions {
|
|
48
|
+
table: string;
|
|
49
|
+
schema: SchemaDefinition;
|
|
50
|
+
lang: string;
|
|
51
|
+
fallbackLang?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Main-table fields that can be safely used as cache fallback in COALESCE.
|
|
54
|
+
* If omitted, all translatable fields are assumed available in main table.
|
|
55
|
+
*/
|
|
56
|
+
mainFallbackFields?: string[];
|
|
57
|
+
where?: Record<string, unknown>;
|
|
58
|
+
orderBy?: OrderByOption[];
|
|
59
|
+
limit?: number;
|
|
60
|
+
offset?: number;
|
|
61
|
+
}
|
|
57
62
|
|
|
58
63
|
/**
|
|
59
64
|
* Query result with SQL and parameters
|
|
@@ -73,12 +78,13 @@ export function buildTranslationQuery(
|
|
|
73
78
|
table,
|
|
74
79
|
schema,
|
|
75
80
|
lang,
|
|
76
|
-
fallbackLang = schema.languages.default,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
fallbackLang = schema.languages.default,
|
|
82
|
+
mainFallbackFields,
|
|
83
|
+
where,
|
|
84
|
+
orderBy,
|
|
85
|
+
limit,
|
|
86
|
+
offset,
|
|
87
|
+
} = options;
|
|
82
88
|
|
|
83
89
|
const tableSchema = schema.tables[table];
|
|
84
90
|
if (!tableSchema) {
|
|
@@ -94,22 +100,32 @@ export function buildTranslationQuery(
|
|
|
94
100
|
const transTable = toTranslationTableName(table);
|
|
95
101
|
const fkName = toTranslationFKName(table);
|
|
96
102
|
|
|
97
|
-
const mainAlias = "m";
|
|
98
|
-
const transAlias = "t";
|
|
99
|
-
const fallbackAlias = "fb";
|
|
103
|
+
const mainAlias = "m";
|
|
104
|
+
const transAlias = "t";
|
|
105
|
+
const fallbackAlias = "fb";
|
|
106
|
+
const mainFallbackSet =
|
|
107
|
+
mainFallbackFields !== undefined
|
|
108
|
+
? new Set(mainFallbackFields)
|
|
109
|
+
: null;
|
|
100
110
|
|
|
101
111
|
// Build SELECT fields
|
|
102
112
|
const selectFields: string[] = [];
|
|
103
113
|
|
|
104
|
-
for (const [fieldName, field] of Object.entries(tableSchema.fields)) {
|
|
105
|
-
if (field.translatable) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
114
|
+
for (const [fieldName, field] of Object.entries(tableSchema.fields)) {
|
|
115
|
+
if (field.translatable) {
|
|
116
|
+
if (mainFallbackSet && !mainFallbackSet.has(fieldName)) {
|
|
117
|
+
selectFields.push(
|
|
118
|
+
`COALESCE(${transAlias}.${fieldName}, ${fallbackAlias}.${fieldName}) as ${fieldName}`
|
|
119
|
+
);
|
|
120
|
+
} else {
|
|
121
|
+
selectFields.push(
|
|
122
|
+
`COALESCE(${transAlias}.${fieldName}, ${fallbackAlias}.${fieldName}, ${mainAlias}.${fieldName}) as ${fieldName}`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
selectFields.push(`${mainAlias}.${fieldName}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
113
129
|
|
|
114
130
|
let sql = `SELECT ${selectFields.join(", ")}
|
|
115
131
|
FROM ${table} ${mainAlias}
|
|
@@ -154,22 +170,24 @@ LEFT JOIN ${transTable} ${fallbackAlias}
|
|
|
154
170
|
/**
|
|
155
171
|
* Build query for single record by ID with translations
|
|
156
172
|
*/
|
|
157
|
-
export function buildTranslationQueryById(
|
|
158
|
-
table: string,
|
|
159
|
-
schema: SchemaDefinition,
|
|
160
|
-
id: number | string,
|
|
161
|
-
lang: string,
|
|
162
|
-
fallbackLang?: string
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
+
export function buildTranslationQueryById(
|
|
174
|
+
table: string,
|
|
175
|
+
schema: SchemaDefinition,
|
|
176
|
+
id: number | string,
|
|
177
|
+
lang: string,
|
|
178
|
+
fallbackLang?: string,
|
|
179
|
+
mainFallbackFields?: string[]
|
|
180
|
+
): TranslationQueryResult {
|
|
181
|
+
return buildTranslationQuery({
|
|
182
|
+
table,
|
|
183
|
+
schema,
|
|
184
|
+
lang,
|
|
185
|
+
fallbackLang,
|
|
186
|
+
mainFallbackFields,
|
|
187
|
+
where: { id },
|
|
188
|
+
limit: 1,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
173
191
|
|
|
174
192
|
/**
|
|
175
193
|
* Build simple query without translations
|