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