@contentrain/query 1.0.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/README.md +134 -0
- package/dist/index.d.mts +28 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +210 -0
- package/dist/index.mjs +185 -0
- package/package.json +27 -0
- package/src/index.test.ts +146 -0
- package/src/index.ts +233 -0
- package/tsconfig.json +5 -0
- package/tsup.config.ts +8 -0
- package/vitest.config.ts +8 -0
package/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# @contentrain/query
|
|
2
|
+
|
|
3
|
+
Query builder for Contentrain SDK.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @contentrain/query @contentrain/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { ContentrainCore } from '@contentrain/core'
|
|
15
|
+
import { ContentrainQuery } from '@contentrain/query'
|
|
16
|
+
|
|
17
|
+
// Initialize core and query
|
|
18
|
+
const core = new ContentrainCore()
|
|
19
|
+
const query = new ContentrainQuery(core)
|
|
20
|
+
|
|
21
|
+
// Basic query
|
|
22
|
+
const posts = await query
|
|
23
|
+
.from('posts')
|
|
24
|
+
.where('status', 'publish')
|
|
25
|
+
.get()
|
|
26
|
+
|
|
27
|
+
// Complex query
|
|
28
|
+
const featuredPosts = await query
|
|
29
|
+
.from('posts')
|
|
30
|
+
.where('status', 'publish')
|
|
31
|
+
.where('featured', true)
|
|
32
|
+
.orderBy('createdAt', 'desc')
|
|
33
|
+
.limit(5)
|
|
34
|
+
.get()
|
|
35
|
+
|
|
36
|
+
// Query with relations
|
|
37
|
+
const postsWithAuthor = await query
|
|
38
|
+
.from('posts')
|
|
39
|
+
.with('author')
|
|
40
|
+
.where('status', 'publish')
|
|
41
|
+
.get()
|
|
42
|
+
|
|
43
|
+
// Query with multiple conditions
|
|
44
|
+
const searchPosts = await query
|
|
45
|
+
.from('posts')
|
|
46
|
+
.where([
|
|
47
|
+
['status', 'publish'],
|
|
48
|
+
['category', 'technology'],
|
|
49
|
+
['title', 'startsWith', 'How to']
|
|
50
|
+
])
|
|
51
|
+
.get()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## API Reference
|
|
55
|
+
|
|
56
|
+
### Constructor
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
constructor(core: ContentrainCore)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Creates a new instance of ContentrainQuery.
|
|
63
|
+
|
|
64
|
+
### Methods
|
|
65
|
+
|
|
66
|
+
#### from
|
|
67
|
+
```typescript
|
|
68
|
+
from(collection: string): ContentrainQuery
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Specifies the collection to query.
|
|
72
|
+
|
|
73
|
+
#### where
|
|
74
|
+
```typescript
|
|
75
|
+
where(field: string, value: any): ContentrainQuery
|
|
76
|
+
where(field: string, operator: FilterOperator, value: any): ContentrainQuery
|
|
77
|
+
where(conditions: [string, any][] | [string, FilterOperator, any][]): ContentrainQuery
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Adds where conditions to the query.
|
|
81
|
+
|
|
82
|
+
#### orderBy
|
|
83
|
+
```typescript
|
|
84
|
+
orderBy(field: string, direction: 'asc' | 'desc' = 'asc'): ContentrainQuery
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Orders the results by a field.
|
|
88
|
+
|
|
89
|
+
#### limit
|
|
90
|
+
```typescript
|
|
91
|
+
limit(count: number): ContentrainQuery
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Limits the number of results.
|
|
95
|
+
|
|
96
|
+
#### offset
|
|
97
|
+
```typescript
|
|
98
|
+
offset(count: number): ContentrainQuery
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Skips the specified number of results.
|
|
102
|
+
|
|
103
|
+
#### with
|
|
104
|
+
```typescript
|
|
105
|
+
with(...relations: string[]): ContentrainQuery
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Includes related content in the results.
|
|
109
|
+
|
|
110
|
+
#### get
|
|
111
|
+
```typescript
|
|
112
|
+
get<T>(): Promise<T[]>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Executes the query and returns the results.
|
|
116
|
+
|
|
117
|
+
### Filter Operators
|
|
118
|
+
|
|
119
|
+
- `equals` (default)
|
|
120
|
+
- `notEquals`
|
|
121
|
+
- `contains`
|
|
122
|
+
- `notContains`
|
|
123
|
+
- `startsWith`
|
|
124
|
+
- `endsWith`
|
|
125
|
+
- `exists`
|
|
126
|
+
- `notExists`
|
|
127
|
+
- `gt` (greater than)
|
|
128
|
+
- `gte` (greater than or equal)
|
|
129
|
+
- `lt` (less than)
|
|
130
|
+
- `lte` (less than or equal)
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { IContentrainCore } from '@contentrain/core';
|
|
2
|
+
import { ContentrainBaseModel, FilterCondition, SortDirection, WithRelation } from '@contentrain/types';
|
|
3
|
+
|
|
4
|
+
declare class ContentrainQuery<T extends ContentrainBaseModel> {
|
|
5
|
+
private core;
|
|
6
|
+
private collection;
|
|
7
|
+
private filters;
|
|
8
|
+
private sorts;
|
|
9
|
+
private relations;
|
|
10
|
+
private limitCount?;
|
|
11
|
+
private skipCount?;
|
|
12
|
+
constructor(core: IContentrainCore | undefined, collection: string);
|
|
13
|
+
where(field: keyof T, operator: FilterCondition<T>['operator'], value: T[keyof T]): this;
|
|
14
|
+
sort(field: keyof T, direction?: SortDirection): this;
|
|
15
|
+
take(limit: number): this;
|
|
16
|
+
offset(skip: number): this;
|
|
17
|
+
with(relation: keyof T): this;
|
|
18
|
+
private getModelMetadata;
|
|
19
|
+
private getRelatedData;
|
|
20
|
+
private evaluateFilter;
|
|
21
|
+
private evaluateSort;
|
|
22
|
+
get(): Promise<T[]>;
|
|
23
|
+
getWithRelations<K extends keyof T>(): Promise<WithRelation<T, K>[]>;
|
|
24
|
+
first(): Promise<T | null>;
|
|
25
|
+
firstWithRelations<K extends keyof T>(): Promise<WithRelation<T, K> | null>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { ContentrainQuery };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { IContentrainCore } from '@contentrain/core';
|
|
2
|
+
import { ContentrainBaseModel, FilterCondition, SortDirection, WithRelation } from '@contentrain/types';
|
|
3
|
+
|
|
4
|
+
declare class ContentrainQuery<T extends ContentrainBaseModel> {
|
|
5
|
+
private core;
|
|
6
|
+
private collection;
|
|
7
|
+
private filters;
|
|
8
|
+
private sorts;
|
|
9
|
+
private relations;
|
|
10
|
+
private limitCount?;
|
|
11
|
+
private skipCount?;
|
|
12
|
+
constructor(core: IContentrainCore | undefined, collection: string);
|
|
13
|
+
where(field: keyof T, operator: FilterCondition<T>['operator'], value: T[keyof T]): this;
|
|
14
|
+
sort(field: keyof T, direction?: SortDirection): this;
|
|
15
|
+
take(limit: number): this;
|
|
16
|
+
offset(skip: number): this;
|
|
17
|
+
with(relation: keyof T): this;
|
|
18
|
+
private getModelMetadata;
|
|
19
|
+
private getRelatedData;
|
|
20
|
+
private evaluateFilter;
|
|
21
|
+
private evaluateSort;
|
|
22
|
+
get(): Promise<T[]>;
|
|
23
|
+
getWithRelations<K extends keyof T>(): Promise<WithRelation<T, K>[]>;
|
|
24
|
+
first(): Promise<T | null>;
|
|
25
|
+
firstWithRelations<K extends keyof T>(): Promise<WithRelation<T, K> | null>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { ContentrainQuery };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
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;
|
|
40
|
+
}
|
|
41
|
+
sort(field, direction = "asc") {
|
|
42
|
+
this.sorts.push({ field, direction });
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
take(limit) {
|
|
46
|
+
this.limitCount = limit;
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
offset(skip) {
|
|
50
|
+
this.skipCount = skip;
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
with(relation) {
|
|
54
|
+
this.relations.push(relation);
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
async getModelMetadata() {
|
|
58
|
+
return this.core.getModelMetadata(this.collection);
|
|
59
|
+
}
|
|
60
|
+
async getRelatedData(item, relation) {
|
|
61
|
+
const metadata = await this.getModelMetadata();
|
|
62
|
+
const fields = metadata.fields;
|
|
63
|
+
const fieldMetadata = fields.find((f) => f.id === relation);
|
|
64
|
+
if (!fieldMetadata) {
|
|
65
|
+
throw new Error(`Field ${relation} not found in model ${metadata.modelId}`);
|
|
66
|
+
}
|
|
67
|
+
if (!fieldMetadata.relation?.model) {
|
|
68
|
+
throw new Error(`Field ${relation} is not a relation`);
|
|
69
|
+
}
|
|
70
|
+
const relatedIds = item[relation];
|
|
71
|
+
if (!relatedIds) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const relatedMetadata = await this.core.getModelMetadata(fieldMetadata.relation.model);
|
|
75
|
+
const hasLocalization = relatedMetadata.localization ?? false;
|
|
76
|
+
const locale = this.core.getLocale();
|
|
77
|
+
if (Array.isArray(relatedIds)) {
|
|
78
|
+
const relatedItems = await Promise.all(
|
|
79
|
+
relatedIds.map(async (id) => {
|
|
80
|
+
try {
|
|
81
|
+
const data = await this.core.getContentById(fieldMetadata.relation.model, id);
|
|
82
|
+
return hasLocalization && locale && typeof data === "object" && locale in data ? data[locale] : data;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
return relatedItems.filter((item2) => item2 !== null);
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const data = await this.core.getContentById(fieldMetadata.relation.model, relatedIds);
|
|
92
|
+
return hasLocalization && locale && typeof data === "object" && locale in data ? data[locale] : data;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
evaluateFilter(item, filter) {
|
|
98
|
+
const { field, operator, value } = filter;
|
|
99
|
+
const itemValue = item[field];
|
|
100
|
+
switch (operator) {
|
|
101
|
+
case "eq":
|
|
102
|
+
return itemValue === value;
|
|
103
|
+
case "neq":
|
|
104
|
+
return itemValue !== value;
|
|
105
|
+
case "gt":
|
|
106
|
+
return typeof itemValue === "number" && typeof value === "number" && itemValue > value;
|
|
107
|
+
case "gte":
|
|
108
|
+
return typeof itemValue === "number" && typeof value === "number" && itemValue >= value;
|
|
109
|
+
case "lt":
|
|
110
|
+
return typeof itemValue === "number" && typeof value === "number" && itemValue < value;
|
|
111
|
+
case "lte":
|
|
112
|
+
return typeof itemValue === "number" && typeof value === "number" && itemValue <= value;
|
|
113
|
+
case "contains":
|
|
114
|
+
return typeof itemValue === "string" && typeof value === "string" && itemValue.includes(value);
|
|
115
|
+
case "startsWith":
|
|
116
|
+
return typeof itemValue === "string" && typeof value === "string" && itemValue.startsWith(value);
|
|
117
|
+
case "endsWith":
|
|
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;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
evaluateSort(a, b, sort) {
|
|
132
|
+
const { field, direction } = sort;
|
|
133
|
+
const aValue = a[field];
|
|
134
|
+
const bValue = b[field];
|
|
135
|
+
if (aValue === bValue) {
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
if (aValue === void 0 || aValue === null) {
|
|
139
|
+
return direction === "asc" ? -1 : 1;
|
|
140
|
+
}
|
|
141
|
+
if (bValue === void 0 || bValue === null) {
|
|
142
|
+
return direction === "asc" ? 1 : -1;
|
|
143
|
+
}
|
|
144
|
+
if (typeof aValue === "string" && typeof bValue === "string") {
|
|
145
|
+
return direction === "asc" ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue);
|
|
146
|
+
}
|
|
147
|
+
if (typeof aValue === "number" && typeof bValue === "number") {
|
|
148
|
+
return direction === "asc" ? aValue - bValue : bValue - aValue;
|
|
149
|
+
}
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
async get() {
|
|
153
|
+
const metadata = await this.getModelMetadata();
|
|
154
|
+
const hasLocalization = metadata.localization ?? false;
|
|
155
|
+
const locale = this.core.getLocale();
|
|
156
|
+
const items = await this.core.getContent(this.collection);
|
|
157
|
+
let result = items.map((item) => hasLocalization && locale && typeof item === "object" && locale in item ? item[locale] : item);
|
|
158
|
+
if (this.filters.length > 0) {
|
|
159
|
+
result = result.filter(
|
|
160
|
+
(item) => this.filters.every((filter) => this.evaluateFilter(item, filter))
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (this.sorts.length > 0) {
|
|
164
|
+
result = result.sort((a, b) => {
|
|
165
|
+
for (const sort of this.sorts) {
|
|
166
|
+
const comparison = this.evaluateSort(a, b, sort);
|
|
167
|
+
if (comparison !== 0) {
|
|
168
|
+
return comparison;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return 0;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (this.skipCount !== void 0) {
|
|
175
|
+
result = result.slice(this.skipCount);
|
|
176
|
+
}
|
|
177
|
+
if (this.limitCount !== void 0) {
|
|
178
|
+
result = result.slice(0, this.limitCount);
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
async getWithRelations() {
|
|
183
|
+
const items = await this.get();
|
|
184
|
+
return Promise.all(
|
|
185
|
+
items.map(async (item) => {
|
|
186
|
+
const result = { ...item };
|
|
187
|
+
await Promise.all(
|
|
188
|
+
this.relations.map(async (relation) => {
|
|
189
|
+
const relatedData = await this.getRelatedData(item, relation);
|
|
190
|
+
const relationKey = `${relation}-data`;
|
|
191
|
+
result[relationKey] = relatedData;
|
|
192
|
+
})
|
|
193
|
+
);
|
|
194
|
+
return result;
|
|
195
|
+
})
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
async first() {
|
|
199
|
+
const items = await this.get();
|
|
200
|
+
return items[0] ?? null;
|
|
201
|
+
}
|
|
202
|
+
async firstWithRelations() {
|
|
203
|
+
const items = await this.getWithRelations();
|
|
204
|
+
return items[0] ?? null;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
208
|
+
0 && (module.exports = {
|
|
209
|
+
ContentrainQuery
|
|
210
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
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;
|
|
16
|
+
}
|
|
17
|
+
sort(field, direction = "asc") {
|
|
18
|
+
this.sorts.push({ field, direction });
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
take(limit) {
|
|
22
|
+
this.limitCount = limit;
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
offset(skip) {
|
|
26
|
+
this.skipCount = skip;
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
with(relation) {
|
|
30
|
+
this.relations.push(relation);
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
async getModelMetadata() {
|
|
34
|
+
return this.core.getModelMetadata(this.collection);
|
|
35
|
+
}
|
|
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}`);
|
|
42
|
+
}
|
|
43
|
+
if (!fieldMetadata.relation?.model) {
|
|
44
|
+
throw new Error(`Field ${relation} is not a relation`);
|
|
45
|
+
}
|
|
46
|
+
const relatedIds = item[relation];
|
|
47
|
+
if (!relatedIds) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
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;
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
return relatedItems.filter((item2) => item2 !== null);
|
|
65
|
+
}
|
|
66
|
+
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;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
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;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
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;
|
|
113
|
+
}
|
|
114
|
+
if (aValue === void 0 || aValue === null) {
|
|
115
|
+
return direction === "asc" ? -1 : 1;
|
|
116
|
+
}
|
|
117
|
+
if (bValue === void 0 || bValue === null) {
|
|
118
|
+
return direction === "asc" ? 1 : -1;
|
|
119
|
+
}
|
|
120
|
+
if (typeof aValue === "string" && typeof bValue === "string") {
|
|
121
|
+
return direction === "asc" ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue);
|
|
122
|
+
}
|
|
123
|
+
if (typeof aValue === "number" && typeof bValue === "number") {
|
|
124
|
+
return direction === "asc" ? aValue - bValue : bValue - aValue;
|
|
125
|
+
}
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
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
|
+
);
|
|
138
|
+
}
|
|
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;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return 0;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (this.skipCount !== void 0) {
|
|
151
|
+
result = result.slice(this.skipCount);
|
|
152
|
+
}
|
|
153
|
+
if (this.limitCount !== void 0) {
|
|
154
|
+
result = result.slice(0, this.limitCount);
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
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
|
+
})
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
async first() {
|
|
175
|
+
const items = await this.get();
|
|
176
|
+
return items[0] ?? null;
|
|
177
|
+
}
|
|
178
|
+
async firstWithRelations() {
|
|
179
|
+
const items = await this.getWithRelations();
|
|
180
|
+
return items[0] ?? null;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
export {
|
|
184
|
+
ContentrainQuery
|
|
185
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@contentrain/query",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsup",
|
|
10
|
+
"dev": "tsup --watch",
|
|
11
|
+
"test": "vitest",
|
|
12
|
+
"test:run": "vitest run",
|
|
13
|
+
"clean": "rm -rf dist"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@contentrain/core": "workspace:*",
|
|
20
|
+
"@contentrain/types": "workspace:*"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"tsup": "^8.3.5",
|
|
24
|
+
"typescript": "^5.7.2",
|
|
25
|
+
"vitest": "^2.1.8"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { IContentrainCore } from '@contentrain/core';
|
|
2
|
+
import type { ContentrainBaseModel, ContentrainModelMetadata } from '@contentrain/types';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { ContentrainQuery } from './index';
|
|
5
|
+
|
|
6
|
+
interface TestModel extends ContentrainBaseModel {
|
|
7
|
+
title: string
|
|
8
|
+
description: string
|
|
9
|
+
icon: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('contentrain query', () => {
|
|
13
|
+
const mockCore: IContentrainCore = {
|
|
14
|
+
getModelMetadata: vi.fn(),
|
|
15
|
+
getContent: vi.fn(),
|
|
16
|
+
getContentById: vi.fn(),
|
|
17
|
+
getAvailableCollections: vi.fn(),
|
|
18
|
+
getLocale: vi.fn(),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const mockModelMetadata: ContentrainModelMetadata = {
|
|
22
|
+
modelId: 'processes',
|
|
23
|
+
fields: [
|
|
24
|
+
{
|
|
25
|
+
id: 'title',
|
|
26
|
+
type: 'string',
|
|
27
|
+
required: true,
|
|
28
|
+
componentId: 'single-line-text',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'description',
|
|
32
|
+
type: 'string',
|
|
33
|
+
required: true,
|
|
34
|
+
componentId: 'multi-line-text',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'icon',
|
|
38
|
+
type: 'string',
|
|
39
|
+
required: true,
|
|
40
|
+
componentId: 'single-line-text',
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
localization: true,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const mockContent: TestModel = {
|
|
47
|
+
ID: '96c64803d441',
|
|
48
|
+
title: 'Research & Analysis',
|
|
49
|
+
description: 'We identify project goals and client needs, conducting in-depth analysis to develop the right, scalable solutions.',
|
|
50
|
+
icon: 'ri-search-eye-line',
|
|
51
|
+
createdAt: '2024-09-26T13:59:00.000Z',
|
|
52
|
+
updatedAt: '2024-10-14T06:46:13.160Z',
|
|
53
|
+
status: 'publish',
|
|
54
|
+
scheduled: false,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const mockLocalizedContent = {
|
|
58
|
+
tr: {
|
|
59
|
+
ID: '96c64803d441',
|
|
60
|
+
title: 'Araştırma ve Analiz',
|
|
61
|
+
description: 'Proje hedeflerini ve müşteri ihtiyaçlarını belirleyerek, doğru ve ölçeklenebilir çözümleri geliştirmek için analizler yapıyoruz.',
|
|
62
|
+
icon: 'ri-search-eye-line',
|
|
63
|
+
createdAt: '2024-09-26T13:59:00.000Z',
|
|
64
|
+
updatedAt: '2024-10-18T10:14:03.251Z',
|
|
65
|
+
status: 'publish',
|
|
66
|
+
scheduled: false,
|
|
67
|
+
},
|
|
68
|
+
en: {
|
|
69
|
+
ID: '96c64803d441',
|
|
70
|
+
title: 'Research & Analysis',
|
|
71
|
+
description: 'We identify project goals and client needs, conducting in-depth analysis to develop the right, scalable solutions.',
|
|
72
|
+
icon: 'ri-search-eye-line',
|
|
73
|
+
createdAt: '2024-09-26T13:59:00.000Z',
|
|
74
|
+
updatedAt: '2024-10-14T06:46:13.160Z',
|
|
75
|
+
status: 'publish',
|
|
76
|
+
scheduled: false,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
vi.clearAllMocks();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should get content with filters', async () => {
|
|
85
|
+
vi.mocked(mockCore.getModelMetadata).mockResolvedValueOnce(mockModelMetadata);
|
|
86
|
+
vi.mocked(mockCore.getContent).mockResolvedValueOnce([mockContent]);
|
|
87
|
+
|
|
88
|
+
const query = new ContentrainQuery<TestModel>(mockCore, 'processes');
|
|
89
|
+
const result = await query.where('title', 'eq', 'Research & Analysis').get();
|
|
90
|
+
|
|
91
|
+
expect(result).toEqual([mockContent]);
|
|
92
|
+
expect(mockCore.getModelMetadata).toHaveBeenCalledTimes(1);
|
|
93
|
+
expect(mockCore.getContent).toHaveBeenCalledTimes(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should get content with sorting', async () => {
|
|
97
|
+
vi.mocked(mockCore.getModelMetadata).mockResolvedValueOnce(mockModelMetadata);
|
|
98
|
+
vi.mocked(mockCore.getContent).mockResolvedValueOnce([mockContent]);
|
|
99
|
+
|
|
100
|
+
const query = new ContentrainQuery<TestModel>(mockCore, 'processes');
|
|
101
|
+
const result = await query.sort('title', 'asc').get();
|
|
102
|
+
|
|
103
|
+
expect(result).toEqual([mockContent]);
|
|
104
|
+
expect(mockCore.getModelMetadata).toHaveBeenCalledTimes(1);
|
|
105
|
+
expect(mockCore.getContent).toHaveBeenCalledTimes(1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should get content with pagination', async () => {
|
|
109
|
+
vi.mocked(mockCore.getModelMetadata).mockResolvedValueOnce(mockModelMetadata);
|
|
110
|
+
vi.mocked(mockCore.getContent).mockResolvedValueOnce([mockContent]);
|
|
111
|
+
|
|
112
|
+
const query = new ContentrainQuery<TestModel>(mockCore, 'processes');
|
|
113
|
+
const result = await query.take(1).offset(0).get();
|
|
114
|
+
|
|
115
|
+
expect(result).toEqual([mockContent]);
|
|
116
|
+
expect(mockCore.getModelMetadata).toHaveBeenCalledTimes(1);
|
|
117
|
+
expect(mockCore.getContent).toHaveBeenCalledTimes(1);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should get first item', async () => {
|
|
121
|
+
vi.mocked(mockCore.getModelMetadata).mockResolvedValueOnce(mockModelMetadata);
|
|
122
|
+
vi.mocked(mockCore.getContent).mockResolvedValueOnce([mockContent]);
|
|
123
|
+
|
|
124
|
+
const query = new ContentrainQuery<TestModel>(mockCore, 'processes');
|
|
125
|
+
const result = await query.first();
|
|
126
|
+
|
|
127
|
+
expect(result).toEqual(mockContent);
|
|
128
|
+
expect(mockCore.getModelMetadata).toHaveBeenCalledTimes(1);
|
|
129
|
+
expect(mockCore.getContent).toHaveBeenCalledTimes(1);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should handle localized content', async () => {
|
|
133
|
+
vi.mocked(mockCore.getModelMetadata).mockResolvedValueOnce(mockModelMetadata);
|
|
134
|
+
vi.mocked(mockCore.getContent).mockResolvedValueOnce([mockLocalizedContent]);
|
|
135
|
+
vi.mocked(mockCore.getLocale).mockReturnValue('tr');
|
|
136
|
+
|
|
137
|
+
const query = new ContentrainQuery<TestModel>(mockCore, 'processes');
|
|
138
|
+
const result = await query.get();
|
|
139
|
+
|
|
140
|
+
expect(result[0].title).toBe('Araştırma ve Analiz');
|
|
141
|
+
expect(result[0].description).toBe('Proje hedeflerini ve müşteri ihtiyaçlarını belirleyerek, doğru ve ölçeklenebilir çözümleri geliştirmek için analizler yapıyoruz.');
|
|
142
|
+
expect(mockCore.getModelMetadata).toHaveBeenCalledTimes(1);
|
|
143
|
+
expect(mockCore.getContent).toHaveBeenCalledTimes(1);
|
|
144
|
+
expect(mockCore.getLocale).toHaveBeenCalledTimes(1);
|
|
145
|
+
});
|
|
146
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import type { IContentrainCore } from '@contentrain/core';
|
|
2
|
+
import type { ContentrainBaseModel, ContentrainModelMetadata, FilterCondition, SortCondition, SortDirection, WithRelation } from '@contentrain/types';
|
|
3
|
+
import { ContentrainCore } from '@contentrain/core';
|
|
4
|
+
|
|
5
|
+
export class ContentrainQuery<T extends ContentrainBaseModel> {
|
|
6
|
+
private filters: FilterCondition<T>[] = [];
|
|
7
|
+
private sorts: SortCondition<T>[] = [];
|
|
8
|
+
private relations: string[] = [];
|
|
9
|
+
private limitCount?: number;
|
|
10
|
+
private skipCount?: number;
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
private core: IContentrainCore = new ContentrainCore(),
|
|
14
|
+
private collection: string,
|
|
15
|
+
) { }
|
|
16
|
+
|
|
17
|
+
where(field: keyof T, operator: FilterCondition<T>['operator'], value: T[keyof T]): this {
|
|
18
|
+
this.filters.push({ field, operator, value });
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
sort(field: keyof T, direction: SortDirection = 'asc'): this {
|
|
23
|
+
this.sorts.push({ field, direction });
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
take(limit: number): this {
|
|
28
|
+
this.limitCount = limit;
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
offset(skip: number): this {
|
|
33
|
+
this.skipCount = skip;
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
with(relation: keyof T): this {
|
|
38
|
+
this.relations.push(relation as string);
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private async getModelMetadata(): Promise<ContentrainModelMetadata> {
|
|
43
|
+
return this.core.getModelMetadata(this.collection);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private async getRelatedData(item: T, relation: string): Promise<ContentrainBaseModel | ContentrainBaseModel[] | null> {
|
|
47
|
+
const metadata = await this.getModelMetadata();
|
|
48
|
+
const fields = metadata.fields;
|
|
49
|
+
const fieldMetadata = fields.find(f => f.id === relation);
|
|
50
|
+
|
|
51
|
+
if (!fieldMetadata) {
|
|
52
|
+
throw new Error(`Field ${relation} not found in model ${metadata.modelId}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!fieldMetadata.relation?.model) {
|
|
56
|
+
throw new Error(`Field ${relation} is not a relation`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const relatedIds = item[relation as keyof T];
|
|
60
|
+
if (!relatedIds) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const relatedMetadata = await this.core.getModelMetadata(fieldMetadata.relation.model);
|
|
65
|
+
const hasLocalization = relatedMetadata.localization ?? false;
|
|
66
|
+
const locale = this.core.getLocale();
|
|
67
|
+
|
|
68
|
+
if (Array.isArray(relatedIds)) {
|
|
69
|
+
const relatedItems = await Promise.all(
|
|
70
|
+
relatedIds.map(async (id: string) => {
|
|
71
|
+
try {
|
|
72
|
+
const data = await this.core.getContentById<ContentrainBaseModel>(fieldMetadata.relation!.model, id);
|
|
73
|
+
return hasLocalization && locale && typeof data === 'object' && locale in data
|
|
74
|
+
? (data as Record<string, ContentrainBaseModel>)[locale]
|
|
75
|
+
: data;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return relatedItems.filter((item): item is ContentrainBaseModel => item !== null);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const data = await this.core.getContentById<ContentrainBaseModel>(fieldMetadata.relation.model, relatedIds as string);
|
|
88
|
+
return hasLocalization && locale && typeof data === 'object' && locale in data
|
|
89
|
+
? (data as Record<string, ContentrainBaseModel>)[locale]
|
|
90
|
+
: data;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private evaluateFilter(item: T, filter: FilterCondition<T>): boolean {
|
|
98
|
+
const { field, operator, value } = filter;
|
|
99
|
+
const itemValue = item[field];
|
|
100
|
+
|
|
101
|
+
switch (operator) {
|
|
102
|
+
case 'eq':
|
|
103
|
+
return itemValue === value;
|
|
104
|
+
case 'neq':
|
|
105
|
+
return itemValue !== value;
|
|
106
|
+
case 'gt':
|
|
107
|
+
return typeof itemValue === 'number' && typeof value === 'number' && itemValue > value;
|
|
108
|
+
case 'gte':
|
|
109
|
+
return typeof itemValue === 'number' && typeof value === 'number' && itemValue >= value;
|
|
110
|
+
case 'lt':
|
|
111
|
+
return typeof itemValue === 'number' && typeof value === 'number' && itemValue < value;
|
|
112
|
+
case 'lte':
|
|
113
|
+
return typeof itemValue === 'number' && typeof value === 'number' && itemValue <= value;
|
|
114
|
+
case 'contains':
|
|
115
|
+
return typeof itemValue === 'string' && typeof value === 'string' && itemValue.includes(value);
|
|
116
|
+
case 'startsWith':
|
|
117
|
+
return typeof itemValue === 'string' && typeof value === 'string' && itemValue.startsWith(value);
|
|
118
|
+
case 'endsWith':
|
|
119
|
+
return typeof itemValue === 'string' && typeof value === 'string' && itemValue.endsWith(value);
|
|
120
|
+
case 'in':
|
|
121
|
+
return Array.isArray(value) && value.includes(itemValue);
|
|
122
|
+
case 'nin':
|
|
123
|
+
return Array.isArray(value) && !value.includes(itemValue);
|
|
124
|
+
case 'exists':
|
|
125
|
+
return itemValue !== undefined && itemValue !== null;
|
|
126
|
+
case 'notExists':
|
|
127
|
+
return itemValue === undefined || itemValue === null;
|
|
128
|
+
default:
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private evaluateSort(a: T, b: T, sort: SortCondition<T>): number {
|
|
134
|
+
const { field, direction } = sort;
|
|
135
|
+
const aValue = a[field];
|
|
136
|
+
const bValue = b[field];
|
|
137
|
+
|
|
138
|
+
if (aValue === bValue) {
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (aValue === undefined || aValue === null) {
|
|
143
|
+
return direction === 'asc' ? -1 : 1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (bValue === undefined || bValue === null) {
|
|
147
|
+
return direction === 'asc' ? 1 : -1;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
|
151
|
+
return direction === 'asc'
|
|
152
|
+
? aValue.localeCompare(bValue)
|
|
153
|
+
: bValue.localeCompare(aValue);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (typeof aValue === 'number' && typeof bValue === 'number') {
|
|
157
|
+
return direction === 'asc'
|
|
158
|
+
? aValue - bValue
|
|
159
|
+
: bValue - aValue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async get(): Promise<T[]> {
|
|
166
|
+
const metadata = await this.getModelMetadata();
|
|
167
|
+
const hasLocalization = metadata.localization ?? false;
|
|
168
|
+
const locale = this.core.getLocale();
|
|
169
|
+
|
|
170
|
+
const items = await this.core.getContent<Record<string, unknown>>(this.collection);
|
|
171
|
+
let result = items.map(item => hasLocalization && locale && typeof item === 'object' && locale in item
|
|
172
|
+
? (item as Record<string, T>)[locale]
|
|
173
|
+
: item as T);
|
|
174
|
+
|
|
175
|
+
if (this.filters.length > 0) {
|
|
176
|
+
result = result.filter(item =>
|
|
177
|
+
this.filters.every(filter => this.evaluateFilter(item, filter)),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (this.sorts.length > 0) {
|
|
182
|
+
result = result.sort((a, b) => {
|
|
183
|
+
for (const sort of this.sorts) {
|
|
184
|
+
const comparison = this.evaluateSort(a, b, sort);
|
|
185
|
+
if (comparison !== 0) {
|
|
186
|
+
return comparison;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return 0;
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (this.skipCount !== undefined) {
|
|
194
|
+
result = result.slice(this.skipCount);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (this.limitCount !== undefined) {
|
|
198
|
+
result = result.slice(0, this.limitCount);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async getWithRelations<K extends keyof T>(): Promise<WithRelation<T, K>[]> {
|
|
205
|
+
const items = await this.get();
|
|
206
|
+
|
|
207
|
+
return Promise.all(
|
|
208
|
+
items.map(async (item) => {
|
|
209
|
+
const result = { ...item } as WithRelation<T, K>;
|
|
210
|
+
|
|
211
|
+
await Promise.all(
|
|
212
|
+
this.relations.map(async (relation) => {
|
|
213
|
+
const relatedData = await this.getRelatedData(item, relation);
|
|
214
|
+
const relationKey = `${relation}-data` as keyof WithRelation<T, K>;
|
|
215
|
+
result[relationKey] = relatedData as any;
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
return result;
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async first(): Promise<T | null> {
|
|
225
|
+
const items = await this.get();
|
|
226
|
+
return items[0] ?? null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async firstWithRelations<K extends keyof T>(): Promise<WithRelation<T, K> | null> {
|
|
230
|
+
const items = await this.getWithRelations<K>();
|
|
231
|
+
return items[0] ?? null;
|
|
232
|
+
}
|
|
233
|
+
}
|
package/tsconfig.json
ADDED
package/tsup.config.ts
ADDED