@contentrain/query 2.0.1 → 3.1.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.
- package/LICENSE +21 -0
- package/README.md +220 -86
- package/dist/index.d.mts +284 -26
- package/dist/index.d.ts +284 -26
- package/dist/index.js +630 -171
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +626 -148
- package/dist/index.mjs.map +1 -0
- package/package.json +37 -13
- package/CHANGELOG.md +0 -27
- package/src/index.test.ts +0 -150
- package/src/index.ts +0 -233
- package/tsconfig.json +0 -5
- package/tsup.config.ts +0 -8
- package/vitest.config.ts +0 -8
package/dist/index.js
CHANGED
|
@@ -1,210 +1,669 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var promises = require('fs/promises');
|
|
4
|
+
var path = require('path');
|
|
5
|
+
var tinyLru = require('tiny-lru');
|
|
6
|
+
|
|
2
7
|
var __defProp = Object.defineProperty;
|
|
3
|
-
var
|
|
4
|
-
var
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
9
|
+
var _MemoryCache = class _MemoryCache {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.stats = {
|
|
12
|
+
hits: 0,
|
|
13
|
+
misses: 0,
|
|
14
|
+
size: 0,
|
|
15
|
+
lastCleanup: Date.now()
|
|
16
|
+
};
|
|
17
|
+
this.options = {
|
|
18
|
+
maxSize: 100,
|
|
19
|
+
// 100 MB
|
|
20
|
+
defaultTTL: 60 * 1e3,
|
|
21
|
+
// 1 dakika
|
|
22
|
+
...options
|
|
23
|
+
};
|
|
24
|
+
const maxItems = Math.floor(this.options.maxSize * 1024 * 1024 / 1e3);
|
|
25
|
+
this.cache = tinyLru.lru(maxItems);
|
|
15
26
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
// src/index.ts
|
|
21
|
-
var index_exports = {};
|
|
22
|
-
__export(index_exports, {
|
|
23
|
-
ContentrainQuery: () => ContentrainQuery
|
|
24
|
-
});
|
|
25
|
-
module.exports = __toCommonJS(index_exports);
|
|
26
|
-
var import_core = require("@contentrain/core");
|
|
27
|
-
var ContentrainQuery = class {
|
|
28
|
-
constructor(core = new import_core.ContentrainCore(), collection) {
|
|
29
|
-
this.core = core;
|
|
30
|
-
this.collection = collection;
|
|
31
|
-
}
|
|
32
|
-
filters = [];
|
|
33
|
-
sorts = [];
|
|
34
|
-
relations = [];
|
|
35
|
-
limitCount;
|
|
36
|
-
skipCount;
|
|
37
|
-
where(field, operator, value) {
|
|
38
|
-
this.filters.push({ field, operator, value });
|
|
39
|
-
return this;
|
|
27
|
+
calculateSize(data) {
|
|
28
|
+
const str = JSON.stringify(data);
|
|
29
|
+
return new TextEncoder().encode(str).length;
|
|
40
30
|
}
|
|
41
|
-
|
|
42
|
-
this.
|
|
43
|
-
|
|
31
|
+
async set(key, data, ttl) {
|
|
32
|
+
await this.cleanupCache();
|
|
33
|
+
const size = this.calculateSize(data);
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
const expireAt = now + (ttl || this.options.defaultTTL);
|
|
36
|
+
while (size + this.stats.size > this.options.maxSize * 1024 * 1024) {
|
|
37
|
+
const oldestKey = this.findOldestKey();
|
|
38
|
+
if (!oldestKey)
|
|
39
|
+
break;
|
|
40
|
+
await this.delete(oldestKey);
|
|
41
|
+
}
|
|
42
|
+
const entry = {
|
|
43
|
+
data,
|
|
44
|
+
expireAt,
|
|
45
|
+
size,
|
|
46
|
+
createdAt: now
|
|
47
|
+
};
|
|
48
|
+
const oldEntry = this.cache.get(key);
|
|
49
|
+
if (oldEntry) {
|
|
50
|
+
this.stats.size -= oldEntry.size;
|
|
51
|
+
}
|
|
52
|
+
this.cache.set(key, entry);
|
|
53
|
+
this.stats.size += size;
|
|
44
54
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
findOldestKey() {
|
|
56
|
+
let oldestKey = null;
|
|
57
|
+
let oldestTime = Infinity;
|
|
58
|
+
for (const key of this.cache.keys()) {
|
|
59
|
+
const entry = this.cache.get(key);
|
|
60
|
+
if (entry.createdAt < oldestTime) {
|
|
61
|
+
oldestTime = entry.createdAt;
|
|
62
|
+
oldestKey = key;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return oldestKey;
|
|
48
66
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
67
|
+
async get(key) {
|
|
68
|
+
const entry = this.cache.get(key);
|
|
69
|
+
if (!entry) {
|
|
70
|
+
this.stats.misses++;
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
if (Date.now() >= entry.expireAt) {
|
|
74
|
+
await this.delete(key);
|
|
75
|
+
this.stats.misses++;
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
this.stats.hits++;
|
|
79
|
+
return entry.data;
|
|
52
80
|
}
|
|
53
|
-
|
|
54
|
-
this.
|
|
55
|
-
|
|
81
|
+
async delete(key) {
|
|
82
|
+
const entry = this.cache.get(key);
|
|
83
|
+
if (entry) {
|
|
84
|
+
this.stats.size -= entry.size;
|
|
85
|
+
this.cache.delete(key);
|
|
86
|
+
}
|
|
56
87
|
}
|
|
57
|
-
async
|
|
58
|
-
|
|
88
|
+
async clear() {
|
|
89
|
+
this.cache.clear();
|
|
90
|
+
this.stats = {
|
|
91
|
+
hits: 0,
|
|
92
|
+
misses: 0,
|
|
93
|
+
size: 0,
|
|
94
|
+
lastCleanup: Date.now()
|
|
95
|
+
};
|
|
59
96
|
}
|
|
60
|
-
async
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
97
|
+
async cleanupCache() {
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
const expiredKeys = [];
|
|
100
|
+
let totalSize = 0;
|
|
101
|
+
for (const key of this.cache.keys()) {
|
|
102
|
+
const entry = this.cache.get(key);
|
|
103
|
+
if (entry.expireAt <= now) {
|
|
104
|
+
expiredKeys.push(key);
|
|
105
|
+
} else {
|
|
106
|
+
totalSize += entry.size;
|
|
107
|
+
}
|
|
66
108
|
}
|
|
67
|
-
|
|
68
|
-
|
|
109
|
+
for (const key of expiredKeys) {
|
|
110
|
+
await this.delete(key);
|
|
69
111
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
112
|
+
while (totalSize > this.options.maxSize * 1024 * 1024) {
|
|
113
|
+
const oldestKey = this.findOldestKey();
|
|
114
|
+
if (!oldestKey)
|
|
115
|
+
break;
|
|
116
|
+
const entry = this.cache.get(oldestKey);
|
|
117
|
+
await this.delete(oldestKey);
|
|
118
|
+
totalSize -= entry.size;
|
|
119
|
+
}
|
|
120
|
+
this.stats.lastCleanup = now;
|
|
121
|
+
}
|
|
122
|
+
getStats() {
|
|
123
|
+
return { ...this.stats };
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
__name(_MemoryCache, "MemoryCache");
|
|
127
|
+
var MemoryCache = _MemoryCache;
|
|
128
|
+
|
|
129
|
+
// src/loader/content.ts
|
|
130
|
+
var _ContentLoader = class _ContentLoader {
|
|
131
|
+
constructor(options) {
|
|
132
|
+
this.modelConfigs = /* @__PURE__ */ new Map();
|
|
133
|
+
this.relations = /* @__PURE__ */ new Map();
|
|
134
|
+
this.options = {
|
|
135
|
+
defaultLocale: "en",
|
|
136
|
+
cache: true,
|
|
137
|
+
ttl: 60 * 1e3,
|
|
138
|
+
// 1 dakika
|
|
139
|
+
maxCacheSize: 100,
|
|
140
|
+
// 100 MB
|
|
141
|
+
...options
|
|
142
|
+
};
|
|
143
|
+
this.cache = new MemoryCache({
|
|
144
|
+
maxSize: this.options.maxCacheSize,
|
|
145
|
+
defaultTTL: this.options.ttl
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
getCacheKey(model) {
|
|
149
|
+
return `${model}`;
|
|
150
|
+
}
|
|
151
|
+
getModelTTL(model) {
|
|
152
|
+
return this.options.modelTTL?.[model] || this.options.ttl || 0;
|
|
153
|
+
}
|
|
154
|
+
async clearCache() {
|
|
155
|
+
await this.cache.clear();
|
|
156
|
+
}
|
|
157
|
+
async refreshCache(model) {
|
|
158
|
+
const cacheKey = this.getCacheKey(model);
|
|
159
|
+
await this.cache.delete(cacheKey);
|
|
160
|
+
await this.load(model);
|
|
161
|
+
}
|
|
162
|
+
getCacheStats() {
|
|
163
|
+
return this.cache.getStats();
|
|
164
|
+
}
|
|
165
|
+
async loadModelConfig(model) {
|
|
166
|
+
try {
|
|
167
|
+
const metadataPath = path.join(this.options.contentDir, "models", "metadata.json");
|
|
168
|
+
const metadataContent = await promises.readFile(metadataPath, "utf-8");
|
|
169
|
+
const allMetadata = JSON.parse(metadataContent);
|
|
170
|
+
const modelMetadata = allMetadata.find((m) => m.modelId === model);
|
|
171
|
+
if (!modelMetadata) {
|
|
172
|
+
throw new Error(`Model metadata not found for ${model}`);
|
|
173
|
+
}
|
|
174
|
+
const modelPath = path.join(this.options.contentDir, "models", `${model}.json`);
|
|
175
|
+
const modelContent = await promises.readFile(modelPath, "utf-8");
|
|
176
|
+
const modelFields = JSON.parse(modelContent);
|
|
177
|
+
return {
|
|
178
|
+
metadata: modelMetadata,
|
|
179
|
+
fields: modelFields
|
|
180
|
+
};
|
|
181
|
+
} catch (error) {
|
|
182
|
+
throw new Error(`Failed to load model config for ${model}: ${error?.message || "Unknown error"}`);
|
|
73
183
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
} catch {
|
|
84
|
-
return null;
|
|
184
|
+
}
|
|
185
|
+
async loadContentFile(model, locale = "default") {
|
|
186
|
+
try {
|
|
187
|
+
const modelConfig = await this.loadModelConfig(model);
|
|
188
|
+
let contentPath;
|
|
189
|
+
if (modelConfig.metadata.localization) {
|
|
190
|
+
if (!locale || locale === "default") {
|
|
191
|
+
if (!this.options.defaultLocale) {
|
|
192
|
+
throw new Error(`Default locale is required for localized model "${model}"`);
|
|
85
193
|
}
|
|
86
|
-
|
|
194
|
+
locale = this.options.defaultLocale;
|
|
195
|
+
}
|
|
196
|
+
contentPath = path.join(this.options.contentDir, model, `${locale}.json`);
|
|
197
|
+
} else {
|
|
198
|
+
if (locale !== "default") {
|
|
199
|
+
console.warn(`Locale "${locale}" specified for non-localized model "${model}". This parameter will be ignored.`);
|
|
200
|
+
}
|
|
201
|
+
contentPath = path.join(this.options.contentDir, model, `${model}.json`);
|
|
202
|
+
}
|
|
203
|
+
const content = await promises.readFile(contentPath, "utf-8");
|
|
204
|
+
try {
|
|
205
|
+
const data = JSON.parse(content);
|
|
206
|
+
return {
|
|
207
|
+
model,
|
|
208
|
+
locale: modelConfig.metadata.localization ? locale : void 0,
|
|
209
|
+
data
|
|
210
|
+
};
|
|
211
|
+
} catch {
|
|
212
|
+
throw new Error(`Failed to load content: Invalid JSON format in ${contentPath}`);
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (error.message.includes("Invalid JSON format")) {
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
throw new Error(
|
|
219
|
+
`Failed to load content file for ${model}${locale ? ` (${locale})` : ""}: ${error?.message || "Unknown error"}`
|
|
87
220
|
);
|
|
88
|
-
return relatedItems.filter((item2) => item2 !== null);
|
|
89
221
|
}
|
|
222
|
+
}
|
|
223
|
+
async loadRelations(model) {
|
|
90
224
|
try {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
225
|
+
const modelConfig = this.modelConfigs.get(model);
|
|
226
|
+
if (!modelConfig) {
|
|
227
|
+
throw new Error(`Model config not found for ${model}`);
|
|
228
|
+
}
|
|
229
|
+
const relationFields = modelConfig.fields.filter((field) => {
|
|
230
|
+
return field.fieldType === "relation";
|
|
231
|
+
});
|
|
232
|
+
return relationFields.map((field) => {
|
|
233
|
+
const options = field.options;
|
|
234
|
+
const reference = options?.reference?.form?.reference?.value;
|
|
235
|
+
if (!reference) {
|
|
236
|
+
throw new Error(`Reference not found for relation field: ${field.name}`);
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
model: reference,
|
|
240
|
+
type: field.componentId === "one-to-one" ? "one-to-one" : "one-to-many",
|
|
241
|
+
foreignKey: field.fieldId
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
} catch (error) {
|
|
245
|
+
throw new Error(`Failed to load relations for ${model}: ${error?.message || "Unknown error"}`);
|
|
95
246
|
}
|
|
96
247
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return typeof itemValue === "string" && typeof value === "string" && itemValue.endsWith(value);
|
|
119
|
-
case "in":
|
|
120
|
-
return Array.isArray(value) && value.includes(itemValue);
|
|
121
|
-
case "nin":
|
|
122
|
-
return Array.isArray(value) && !value.includes(itemValue);
|
|
123
|
-
case "exists":
|
|
124
|
-
return itemValue !== void 0 && itemValue !== null;
|
|
125
|
-
case "notExists":
|
|
126
|
-
return itemValue === void 0 || itemValue === null;
|
|
127
|
-
default:
|
|
128
|
-
return false;
|
|
248
|
+
async getModelLocales(model, modelConfig) {
|
|
249
|
+
try {
|
|
250
|
+
if (!modelConfig.metadata.localization) {
|
|
251
|
+
return ["default"];
|
|
252
|
+
}
|
|
253
|
+
const modelDir = path.join(this.options.contentDir, model);
|
|
254
|
+
const files = await promises.readdir(modelDir);
|
|
255
|
+
const locales = files.filter((file) => file.endsWith(".json")).map((file) => file.replace(".json", "")).filter((locale) => locale !== model);
|
|
256
|
+
if (locales.length === 0) {
|
|
257
|
+
if (!this.options.defaultLocale) {
|
|
258
|
+
throw new Error(`No locale files found for localized model "${model}" and no default locale specified`);
|
|
259
|
+
}
|
|
260
|
+
return [this.options.defaultLocale];
|
|
261
|
+
}
|
|
262
|
+
return locales;
|
|
263
|
+
} catch (error) {
|
|
264
|
+
if (!this.options.defaultLocale) {
|
|
265
|
+
throw new Error(`Failed to read locales for model ${model} and no default locale specified: ${error?.message}`);
|
|
266
|
+
}
|
|
267
|
+
console.warn(`Failed to read locales for model ${model}: ${error?.message}`);
|
|
268
|
+
return [this.options.defaultLocale];
|
|
129
269
|
}
|
|
130
270
|
}
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
271
|
+
async load(model) {
|
|
272
|
+
const cacheKey = `${model}`;
|
|
273
|
+
if (this.options.cache) {
|
|
274
|
+
const cached = await this.cache.get(cacheKey);
|
|
275
|
+
if (cached)
|
|
276
|
+
return cached;
|
|
277
|
+
}
|
|
278
|
+
const modelConfig = await this.loadModelConfig(model);
|
|
279
|
+
this.modelConfigs.set(model, modelConfig);
|
|
280
|
+
const relations = await this.loadRelations(model);
|
|
281
|
+
this.relations.set(model, relations);
|
|
282
|
+
const content = {};
|
|
283
|
+
if (modelConfig.metadata.localization) {
|
|
284
|
+
const locales = await this.getModelLocales(model, modelConfig);
|
|
285
|
+
for (const locale of locales) {
|
|
286
|
+
try {
|
|
287
|
+
const file = await this.loadContentFile(model, locale);
|
|
288
|
+
content[locale] = file.data;
|
|
289
|
+
} catch (error) {
|
|
290
|
+
console.warn(`Failed to load content for locale ${locale}: ${error?.message}`);
|
|
291
|
+
if (locale === this.options.defaultLocale) {
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
const file = await this.loadContentFile(model);
|
|
298
|
+
content.default = file.data;
|
|
137
299
|
}
|
|
138
|
-
|
|
139
|
-
|
|
300
|
+
let assets;
|
|
301
|
+
try {
|
|
302
|
+
const assetsPath = path.join(this.options.contentDir, "assets.json");
|
|
303
|
+
const assetsContent = await promises.readFile(assetsPath, "utf-8");
|
|
304
|
+
assets = JSON.parse(assetsContent);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.warn("Assets file not found or cannot be read:", error);
|
|
140
307
|
}
|
|
141
|
-
|
|
142
|
-
|
|
308
|
+
const result = {
|
|
309
|
+
model: modelConfig,
|
|
310
|
+
content,
|
|
311
|
+
assets
|
|
312
|
+
};
|
|
313
|
+
if (this.options.cache) {
|
|
314
|
+
const ttl = this.getModelTTL(model);
|
|
315
|
+
await this.cache.set(cacheKey, result, ttl);
|
|
143
316
|
}
|
|
144
|
-
|
|
145
|
-
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
async resolveRelation(model, relationField, data, locale) {
|
|
320
|
+
try {
|
|
321
|
+
const relations = this.relations.get(model);
|
|
322
|
+
if (!relations)
|
|
323
|
+
throw new Error(`No relations found for model: ${model}`);
|
|
324
|
+
const relation = relations.find((r) => r.foreignKey === relationField);
|
|
325
|
+
if (!relation)
|
|
326
|
+
throw new Error(`No relation found for field: ${String(relationField)}`);
|
|
327
|
+
const relatedContent = await this.load(relation.model);
|
|
328
|
+
const relatedData = locale ? relatedContent.content[locale] : relatedContent.content.en;
|
|
329
|
+
if (!relatedData) {
|
|
330
|
+
throw new Error(`Failed to resolve relation: No data found for model ${relation.model}`);
|
|
331
|
+
}
|
|
332
|
+
if (relation.type === "one-to-one") {
|
|
333
|
+
return data.map((item) => {
|
|
334
|
+
const relatedItem = relatedData.find((r) => r.ID === item[relationField]);
|
|
335
|
+
if (!relatedItem) {
|
|
336
|
+
throw new Error(`Failed to resolve relation: No matching item found for ID ${String(item[relationField])}`);
|
|
337
|
+
}
|
|
338
|
+
return relatedItem;
|
|
339
|
+
});
|
|
340
|
+
} else {
|
|
341
|
+
const uniqueIds = new Set(
|
|
342
|
+
data.flatMap(
|
|
343
|
+
(item) => Array.isArray(item[relationField]) ? item[relationField] : [item[relationField]]
|
|
344
|
+
)
|
|
345
|
+
);
|
|
346
|
+
const items = Array.from(uniqueIds).map((id) => relatedData.find((r) => r.ID === id)).filter(Boolean);
|
|
347
|
+
if (items.length !== uniqueIds.size) {
|
|
348
|
+
throw new Error("Failed to resolve relation: Some related items not found");
|
|
349
|
+
}
|
|
350
|
+
return items;
|
|
351
|
+
}
|
|
352
|
+
} catch (error) {
|
|
353
|
+
throw new Error(`Failed to resolve relation: ${error.message}`);
|
|
146
354
|
}
|
|
147
|
-
|
|
148
|
-
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
__name(_ContentLoader, "ContentLoader");
|
|
358
|
+
var ContentLoader = _ContentLoader;
|
|
359
|
+
|
|
360
|
+
// src/query/builder.ts
|
|
361
|
+
var _ContentrainQueryBuilder = class _ContentrainQueryBuilder {
|
|
362
|
+
constructor(model, executor, loader) {
|
|
363
|
+
this.filters = [];
|
|
364
|
+
this.includes = {};
|
|
365
|
+
this.sorting = [];
|
|
366
|
+
this.pagination = {};
|
|
367
|
+
this.options = {};
|
|
368
|
+
this.model = model;
|
|
369
|
+
this.executor = executor;
|
|
370
|
+
this.loader = loader;
|
|
371
|
+
}
|
|
372
|
+
where(field, operator, value) {
|
|
373
|
+
this.filters.push({
|
|
374
|
+
field,
|
|
375
|
+
operator,
|
|
376
|
+
value
|
|
377
|
+
});
|
|
378
|
+
return this;
|
|
379
|
+
}
|
|
380
|
+
include(relation) {
|
|
381
|
+
if (typeof relation === "string") {
|
|
382
|
+
this.includes[relation] = {};
|
|
383
|
+
} else if (Array.isArray(relation)) {
|
|
384
|
+
relation.forEach((r) => {
|
|
385
|
+
this.includes[r] = {};
|
|
386
|
+
});
|
|
149
387
|
}
|
|
150
|
-
return
|
|
388
|
+
return this;
|
|
389
|
+
}
|
|
390
|
+
orderBy(field, direction = "asc") {
|
|
391
|
+
this.sorting.push({
|
|
392
|
+
field,
|
|
393
|
+
direction
|
|
394
|
+
});
|
|
395
|
+
return this;
|
|
396
|
+
}
|
|
397
|
+
limit(count) {
|
|
398
|
+
this.pagination.limit = count;
|
|
399
|
+
return this;
|
|
400
|
+
}
|
|
401
|
+
offset(count) {
|
|
402
|
+
this.pagination.offset = count;
|
|
403
|
+
return this;
|
|
404
|
+
}
|
|
405
|
+
locale(code) {
|
|
406
|
+
this.options.locale = code;
|
|
407
|
+
return this;
|
|
408
|
+
}
|
|
409
|
+
cache(ttl) {
|
|
410
|
+
this.options.cache = true;
|
|
411
|
+
if (ttl)
|
|
412
|
+
this.options.ttl = ttl;
|
|
413
|
+
return this;
|
|
414
|
+
}
|
|
415
|
+
noCache() {
|
|
416
|
+
this.options.cache = false;
|
|
417
|
+
return this;
|
|
418
|
+
}
|
|
419
|
+
bypassCache() {
|
|
420
|
+
this.options.cache = false;
|
|
421
|
+
this.options.ttl = 0;
|
|
422
|
+
return this;
|
|
423
|
+
}
|
|
424
|
+
toJSON() {
|
|
425
|
+
return {
|
|
426
|
+
model: this.model,
|
|
427
|
+
filters: this.filters,
|
|
428
|
+
includes: this.includes,
|
|
429
|
+
sorting: this.sorting,
|
|
430
|
+
pagination: this.pagination,
|
|
431
|
+
options: this.options
|
|
432
|
+
};
|
|
151
433
|
}
|
|
152
434
|
async get() {
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
435
|
+
const result = await this.loader.load(this.model);
|
|
436
|
+
const modelConfig = result.model;
|
|
437
|
+
let data;
|
|
438
|
+
if (modelConfig.metadata.localization) {
|
|
439
|
+
const locale = this.options.locale || "en";
|
|
440
|
+
data = result.content[locale];
|
|
441
|
+
if (!data) {
|
|
442
|
+
throw new Error(`Content not found for locale: ${locale}`);
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
if (!result.content.default) {
|
|
446
|
+
throw new Error(`Content not found for model: ${this.model}`);
|
|
447
|
+
}
|
|
448
|
+
data = result.content.default;
|
|
162
449
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
450
|
+
return this.executor.execute({
|
|
451
|
+
model: this.model,
|
|
452
|
+
data,
|
|
453
|
+
filters: this.filters,
|
|
454
|
+
includes: this.includes,
|
|
455
|
+
sorting: this.sorting,
|
|
456
|
+
pagination: this.pagination,
|
|
457
|
+
options: this.options
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
async first() {
|
|
461
|
+
const result = await this.limit(1).get();
|
|
462
|
+
return result.data[0] || null;
|
|
463
|
+
}
|
|
464
|
+
async count() {
|
|
465
|
+
const result = await this.get();
|
|
466
|
+
return result.total;
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
__name(_ContentrainQueryBuilder, "ContentrainQueryBuilder");
|
|
470
|
+
var ContentrainQueryBuilder = _ContentrainQueryBuilder;
|
|
471
|
+
|
|
472
|
+
// src/query/executor.ts
|
|
473
|
+
var _QueryExecutor = class _QueryExecutor {
|
|
474
|
+
constructor(loader) {
|
|
475
|
+
this.loader = loader;
|
|
476
|
+
}
|
|
477
|
+
applyFilters(data, filters) {
|
|
478
|
+
return data.filter((item) => {
|
|
479
|
+
return filters.every(({ field, operator, value }) => {
|
|
480
|
+
const itemValue = item[field];
|
|
481
|
+
const validOperators = ["eq", "ne", "gt", "gte", "lt", "lte", "in", "nin", "contains", "startsWith", "endsWith"];
|
|
482
|
+
if (!validOperators.includes(operator)) {
|
|
483
|
+
throw new Error(`Invalid operator: ${operator}`);
|
|
484
|
+
}
|
|
485
|
+
if (typeof itemValue === "string" && typeof value === "string") {
|
|
486
|
+
return this.applyStringOperation(itemValue, operator, value);
|
|
487
|
+
}
|
|
488
|
+
if (Array.isArray(value)) {
|
|
489
|
+
switch (operator) {
|
|
490
|
+
case "in":
|
|
491
|
+
return value.includes(itemValue);
|
|
492
|
+
case "nin":
|
|
493
|
+
return !value.includes(itemValue);
|
|
494
|
+
default:
|
|
495
|
+
throw new Error(`Invalid array operator: ${operator}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (Array.isArray(itemValue)) {
|
|
499
|
+
switch (operator) {
|
|
500
|
+
case "in":
|
|
501
|
+
return value.some((v) => itemValue.includes(v));
|
|
502
|
+
case "nin":
|
|
503
|
+
return !value.some((v) => itemValue.includes(v));
|
|
504
|
+
default:
|
|
505
|
+
throw new Error(`Invalid array operator: ${operator}`);
|
|
169
506
|
}
|
|
170
507
|
}
|
|
171
|
-
|
|
508
|
+
if (typeof itemValue === "number" && typeof value === "number") {
|
|
509
|
+
switch (operator) {
|
|
510
|
+
case "eq":
|
|
511
|
+
return itemValue === value;
|
|
512
|
+
case "ne":
|
|
513
|
+
return itemValue !== value;
|
|
514
|
+
case "gt":
|
|
515
|
+
return itemValue > value;
|
|
516
|
+
case "gte":
|
|
517
|
+
return itemValue >= value;
|
|
518
|
+
case "lt":
|
|
519
|
+
return itemValue < value;
|
|
520
|
+
case "lte":
|
|
521
|
+
return itemValue <= value;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return false;
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
applySorting(data, sorting) {
|
|
529
|
+
return [...data].sort((a, b) => {
|
|
530
|
+
for (const { field, direction } of sorting) {
|
|
531
|
+
if (!(field in a)) {
|
|
532
|
+
throw new Error(`Invalid sort field: ${field}`);
|
|
533
|
+
}
|
|
534
|
+
const aValue = a[field];
|
|
535
|
+
const bValue = b[field];
|
|
536
|
+
if (aValue === bValue)
|
|
537
|
+
continue;
|
|
538
|
+
const compareResult = aValue < bValue ? -1 : 1;
|
|
539
|
+
return direction === "asc" ? compareResult : -compareResult;
|
|
540
|
+
}
|
|
541
|
+
return 0;
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
applyPagination(data, limit, offset = 0) {
|
|
545
|
+
if (!limit)
|
|
546
|
+
return data.slice(offset);
|
|
547
|
+
return data.slice(offset, offset + limit);
|
|
548
|
+
}
|
|
549
|
+
async resolveIncludes(model, data, includes, options) {
|
|
550
|
+
const result = [...data];
|
|
551
|
+
for (const [field, config] of Object.entries(includes)) {
|
|
552
|
+
const relations = await this.loader.resolveRelation(
|
|
553
|
+
model,
|
|
554
|
+
field,
|
|
555
|
+
result,
|
|
556
|
+
options.locale
|
|
557
|
+
);
|
|
558
|
+
if (config.include && relations.length) {
|
|
559
|
+
await this.resolveIncludes(
|
|
560
|
+
field,
|
|
561
|
+
relations,
|
|
562
|
+
config.include,
|
|
563
|
+
options
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
result.forEach((item) => {
|
|
567
|
+
const value = item[field];
|
|
568
|
+
const relatedItems = relations.filter((r) => {
|
|
569
|
+
if (Array.isArray(value)) {
|
|
570
|
+
return value.includes(r.ID);
|
|
571
|
+
}
|
|
572
|
+
return r.ID === value;
|
|
573
|
+
});
|
|
574
|
+
if (!item._relations) {
|
|
575
|
+
item._relations = {};
|
|
576
|
+
}
|
|
577
|
+
item._relations[field] = Array.isArray(value) ? relatedItems : relatedItems[0];
|
|
172
578
|
});
|
|
173
579
|
}
|
|
174
|
-
|
|
175
|
-
|
|
580
|
+
return result;
|
|
581
|
+
}
|
|
582
|
+
applyStringOperation(value, operator, searchValue) {
|
|
583
|
+
switch (operator) {
|
|
584
|
+
case "eq":
|
|
585
|
+
return value === searchValue;
|
|
586
|
+
case "ne":
|
|
587
|
+
return value !== searchValue;
|
|
588
|
+
case "contains":
|
|
589
|
+
return value.toLowerCase().includes(searchValue.toLowerCase());
|
|
590
|
+
case "startsWith":
|
|
591
|
+
return value.toLowerCase().startsWith(searchValue.toLowerCase());
|
|
592
|
+
case "endsWith":
|
|
593
|
+
return value.toLowerCase().endsWith(searchValue.toLowerCase());
|
|
594
|
+
default: {
|
|
595
|
+
const _exhaustiveCheck = operator;
|
|
596
|
+
return _exhaustiveCheck;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
async execute({
|
|
601
|
+
model,
|
|
602
|
+
data,
|
|
603
|
+
filters = [],
|
|
604
|
+
includes = {},
|
|
605
|
+
sorting = [],
|
|
606
|
+
pagination = {},
|
|
607
|
+
options = {}
|
|
608
|
+
}) {
|
|
609
|
+
let result = [...data];
|
|
610
|
+
if (filters.length) {
|
|
611
|
+
result = this.applyFilters(result, filters);
|
|
176
612
|
}
|
|
177
|
-
if (
|
|
178
|
-
result =
|
|
613
|
+
if (Object.keys(includes).length) {
|
|
614
|
+
result = await this.resolveIncludes(model, result, includes, options);
|
|
179
615
|
}
|
|
180
|
-
|
|
616
|
+
if (sorting.length) {
|
|
617
|
+
result = this.applySorting(result, sorting);
|
|
618
|
+
}
|
|
619
|
+
const paginatedData = this.applyPagination(result, pagination.limit, pagination.offset);
|
|
620
|
+
return {
|
|
621
|
+
data: paginatedData,
|
|
622
|
+
total: result.length,
|
|
623
|
+
pagination: pagination.limit ? {
|
|
624
|
+
limit: pagination.limit,
|
|
625
|
+
offset: pagination.offset || 0,
|
|
626
|
+
hasMore: (pagination.offset || 0) + paginatedData.length < result.length
|
|
627
|
+
} : undefined
|
|
628
|
+
};
|
|
181
629
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
630
|
+
};
|
|
631
|
+
__name(_QueryExecutor, "QueryExecutor");
|
|
632
|
+
var QueryExecutor = _QueryExecutor;
|
|
633
|
+
|
|
634
|
+
// src/index.ts
|
|
635
|
+
var _ContentrainSDK = class _ContentrainSDK {
|
|
636
|
+
constructor(options) {
|
|
637
|
+
this.loader = new ContentLoader(options);
|
|
638
|
+
this.executor = new QueryExecutor(this.loader);
|
|
639
|
+
}
|
|
640
|
+
query(model) {
|
|
641
|
+
return new ContentrainQueryBuilder(
|
|
642
|
+
model,
|
|
643
|
+
this.executor,
|
|
644
|
+
this.loader
|
|
196
645
|
);
|
|
197
646
|
}
|
|
198
|
-
async
|
|
199
|
-
|
|
200
|
-
|
|
647
|
+
async load(model) {
|
|
648
|
+
return this.loader.load(model);
|
|
649
|
+
}
|
|
650
|
+
async clearCache() {
|
|
651
|
+
return this.loader.clearCache();
|
|
201
652
|
}
|
|
202
|
-
async
|
|
203
|
-
|
|
204
|
-
|
|
653
|
+
async refreshCache(model) {
|
|
654
|
+
return this.loader.refreshCache(model);
|
|
655
|
+
}
|
|
656
|
+
getCacheStats() {
|
|
657
|
+
return this.loader.getCacheStats();
|
|
205
658
|
}
|
|
206
659
|
};
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
660
|
+
__name(_ContentrainSDK, "ContentrainSDK");
|
|
661
|
+
var ContentrainSDK = _ContentrainSDK;
|
|
662
|
+
|
|
663
|
+
exports.ContentLoader = ContentLoader;
|
|
664
|
+
exports.ContentrainQueryBuilder = ContentrainQueryBuilder;
|
|
665
|
+
exports.ContentrainSDK = ContentrainSDK;
|
|
666
|
+
exports.MemoryCache = MemoryCache;
|
|
667
|
+
exports.QueryExecutor = QueryExecutor;
|
|
668
|
+
//# sourceMappingURL=index.js.map
|
|
669
|
+
//# sourceMappingURL=index.js.map
|