includio-cms 0.16.0 → 0.18.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/CHANGELOG.md +80 -0
- package/DOCS.md +1 -1
- package/dist/admin/api/rest/routes/collections.js +1 -1
- package/dist/admin/api/rest/routes/entries.js +1 -1
- package/dist/admin/api/rest/routes/singletons.js +1 -1
- package/dist/admin/remote/entry.remote.js +20 -3
- package/dist/admin/remote/invite.d.ts +1 -1
- package/dist/admin/remote/preview.remote.js +10 -2
- package/dist/admin/remote/reorder.js +1 -1
- package/dist/admin/remote/shop.remote.d.ts +18 -18
- package/dist/cms/runtime/api.d.ts +10 -6
- package/dist/cms/runtime/api.js +7 -7
- package/dist/components/ui/accordion/accordion.svelte.d.ts +1 -1
- package/dist/components/ui/calendar/calendar.svelte.d.ts +1 -1
- package/dist/components/ui/command/command-dialog.svelte.d.ts +1 -1
- package/dist/components/ui/command/command-input.svelte.d.ts +1 -1
- package/dist/components/ui/command/command.svelte.d.ts +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte.d.ts +1 -1
- package/dist/components/ui/input/input.svelte.d.ts +1 -1
- package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
- package/dist/components/ui/input-group/input-group-textarea.svelte.d.ts +1 -1
- package/dist/components/ui/radio-group/radio-group.svelte.d.ts +1 -1
- package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
- package/dist/components/ui/tabs/tabs.svelte.d.ts +1 -1
- package/dist/components/ui/textarea/textarea.svelte.d.ts +1 -1
- package/dist/components/ui/toggle-group/toggle-group-item.svelte.d.ts +1 -1
- package/dist/components/ui/toggle-group/toggle-group.svelte.d.ts +1 -1
- package/dist/core/server/entries/operations/create.js +1 -1
- package/dist/core/server/entries/operations/get.d.ts +20 -16
- package/dist/core/server/entries/operations/get.js +45 -214
- package/dist/core/server/entries/operations/resolveEntry.d.ts +94 -0
- package/dist/core/server/entries/operations/resolveEntry.js +210 -0
- package/dist/core/server/entries/operations/update.js +1 -1
- package/dist/core/server/fields/populateEntry.d.ts +9 -1
- package/dist/core/server/fields/populateEntry.js +22 -18
- package/dist/core/server/fields/resolveRelationFields.d.ts +2 -1
- package/dist/core/server/fields/resolveRelationFields.js +140 -34
- package/dist/core/server/fields/resolveRichtextLinks.d.ts +2 -1
- package/dist/core/server/fields/resolveRichtextLinks.js +2 -1
- package/dist/core/server/fields/resolveUrlFields.d.ts +2 -1
- package/dist/core/server/fields/resolveUrlFields.js +6 -5
- package/dist/core/server/generator/generator.js +17 -14
- package/dist/entity/index.js +1 -1
- package/dist/shop/server/populate.d.ts +2 -1
- package/dist/shop/server/populate.js +2 -1
- package/dist/sveltekit/server/index.d.ts +1 -1
- package/dist/sveltekit/server/index.js +1 -1
- package/dist/types/plugins.d.ts +6 -2
- package/dist/updates/0.17.0/index.d.ts +2 -0
- package/dist/updates/0.17.0/index.js +78 -0
- package/dist/updates/index.js +2 -1
- package/package.json +1 -1
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { getCMS } from '../../../cms.js';
|
|
2
2
|
import { getAtPath } from '../../../../admin/utils/objectPath.js';
|
|
3
|
-
import { populateEntryData } from '../../fields/populateEntry.js';
|
|
4
3
|
import { getFieldsFromConfig } from '../../../fields/layoutUtils.js';
|
|
5
4
|
import { getEntrySlugPath, getSlugFromEntryData, getEntryPath } from '../../fields/slugResolver.js';
|
|
6
|
-
export const
|
|
5
|
+
export const _getDbEntries = async (options) => {
|
|
7
6
|
return getCMS().databaseAdapter.getEntries(options);
|
|
8
7
|
};
|
|
9
|
-
export const
|
|
8
|
+
export const _countDbEntries = async (options) => {
|
|
10
9
|
return getCMS().databaseAdapter.countEntries(options);
|
|
11
10
|
};
|
|
12
|
-
export const
|
|
11
|
+
export const _getDbEntry = async (options) => {
|
|
13
12
|
const { id, ...rest } = options;
|
|
14
13
|
const [entry] = await getCMS().databaseAdapter.getEntries({
|
|
15
14
|
...rest,
|
|
@@ -17,15 +16,15 @@ export const getDbEntry = async (options) => {
|
|
|
17
16
|
});
|
|
18
17
|
return entry || null;
|
|
19
18
|
};
|
|
20
|
-
export const
|
|
21
|
-
const entry = await
|
|
19
|
+
export const _getDbEntryOrThrow = async (options) => {
|
|
20
|
+
const entry = await _getDbEntry(options);
|
|
22
21
|
if (!entry) {
|
|
23
22
|
throw new Error('Entry not found');
|
|
24
23
|
}
|
|
25
24
|
return entry;
|
|
26
25
|
};
|
|
27
|
-
export const
|
|
28
|
-
return
|
|
26
|
+
export const _countRawEntries = async (options) => {
|
|
27
|
+
return _countDbEntries(options);
|
|
29
28
|
};
|
|
30
29
|
/** Helper: group versions by lang into per-lang published/scheduled/draft maps */
|
|
31
30
|
function buildPerLangVersionMaps(versions) {
|
|
@@ -33,7 +32,6 @@ function buildPerLangVersionMaps(versions) {
|
|
|
33
32
|
const publishedVersions = {};
|
|
34
33
|
const scheduledVersions = {};
|
|
35
34
|
const draftVersions = {};
|
|
36
|
-
// Group versions by lang
|
|
37
35
|
const byLang = new Map();
|
|
38
36
|
for (const v of versions) {
|
|
39
37
|
const arr = byLang.get(v.lang) || [];
|
|
@@ -42,11 +40,8 @@ function buildPerLangVersionMaps(versions) {
|
|
|
42
40
|
}
|
|
43
41
|
for (const [lang, langVersions] of byLang) {
|
|
44
42
|
const sorted = langVersions.sort((a, b) => b.versionNumber - a.versionNumber);
|
|
45
|
-
// Find latest published (publishedAt <= now)
|
|
46
43
|
const published = sorted.find((v) => v.publishedAt != null && v.publishedAt <= now) || null;
|
|
47
|
-
// Find scheduled (publishedAt > now)
|
|
48
44
|
const scheduled = sorted.find((v) => v.publishedAt != null && v.publishedAt > now) || null;
|
|
49
|
-
// Draft = latest version without publishedAt, or latest version newer than published
|
|
50
45
|
const draft = sorted.find((v) => v.publishedAt == null) || null;
|
|
51
46
|
publishedVersions[lang] = published;
|
|
52
47
|
scheduledVersions[lang] = scheduled;
|
|
@@ -54,8 +49,8 @@ function buildPerLangVersionMaps(versions) {
|
|
|
54
49
|
}
|
|
55
50
|
return { publishedVersions, scheduledVersions, draftVersions };
|
|
56
51
|
}
|
|
57
|
-
export const
|
|
58
|
-
const dbEntries = await
|
|
52
|
+
export const _getRawEntries = async (options) => {
|
|
53
|
+
const dbEntries = await _getDbEntries(options);
|
|
59
54
|
const entries = await Promise.all(dbEntries.map(async (entry) => {
|
|
60
55
|
try {
|
|
61
56
|
const versions = await getCMS().databaseAdapter.getEntryVersions({
|
|
@@ -78,203 +73,31 @@ export const getRawEntries = async (options) => {
|
|
|
78
73
|
}));
|
|
79
74
|
return entries.filter((e) => e !== null);
|
|
80
75
|
};
|
|
81
|
-
export const
|
|
82
|
-
const [entry] = await
|
|
76
|
+
export const _getRawEntry = async (options) => {
|
|
77
|
+
const [entry] = await _getRawEntries({
|
|
83
78
|
...options,
|
|
84
79
|
ids: options.id ? [options.id] : undefined,
|
|
85
80
|
includeArchived: options.includeArchived
|
|
86
81
|
});
|
|
87
82
|
return entry || null;
|
|
88
83
|
};
|
|
89
|
-
export const
|
|
90
|
-
const entry = await
|
|
91
|
-
if (!entry) {
|
|
92
|
-
throw new Error('Entry not found');
|
|
93
|
-
}
|
|
94
|
-
return entry;
|
|
95
|
-
};
|
|
96
|
-
export const getEntries = async (options = {}) => {
|
|
97
|
-
const cms = getCMS();
|
|
98
|
-
const language = options.language || cms.languages[0];
|
|
99
|
-
const status = options.status || 'published';
|
|
100
|
-
// Fast path: DB-level pagination when limit/offset provided and adapter supports it
|
|
101
|
-
if (options.limit != null && cms.databaseAdapter.getPaginatedEntries) {
|
|
102
|
-
const rows = await cms.databaseAdapter.getPaginatedEntries({
|
|
103
|
-
slug: options.slug,
|
|
104
|
-
ids: options.ids,
|
|
105
|
-
language,
|
|
106
|
-
status,
|
|
107
|
-
dataValues: options.dataValues,
|
|
108
|
-
dataLike: options.dataLike,
|
|
109
|
-
dataILikeOr: options.dataILikeOr,
|
|
110
|
-
orderBy: options.orderBy,
|
|
111
|
-
dataOrderBy: options.dataOrderBy,
|
|
112
|
-
limit: options.limit,
|
|
113
|
-
offset: options.offset ?? 0
|
|
114
|
-
});
|
|
115
|
-
const entries = await Promise.all(rows.map(async ({ entry, version }) => {
|
|
116
|
-
try {
|
|
117
|
-
const config = cms.getBySlug(entry.slug);
|
|
118
|
-
const fields = getFieldsFromConfig(config);
|
|
119
|
-
const populatedData = await populateEntryData(version.data, fields, language, entry.id);
|
|
120
|
-
const slugPath = getEntrySlugPath(entry.slug);
|
|
121
|
-
const slug = getSlugFromEntryData(version.data, slugPath, language);
|
|
122
|
-
const _url = slug ? getEntryPath(entry.slug, slug) : undefined;
|
|
123
|
-
const result = {
|
|
124
|
-
_id: entry.id,
|
|
125
|
-
_slug: entry.slug,
|
|
126
|
-
_type: entry.type,
|
|
127
|
-
_publishedAt: version.publishedAt,
|
|
128
|
-
_url,
|
|
129
|
-
...populatedData
|
|
130
|
-
};
|
|
131
|
-
if (config.type === 'collection' && config.orderable) {
|
|
132
|
-
result._sortOrder = entry.sortOrder;
|
|
133
|
-
}
|
|
134
|
-
return result;
|
|
135
|
-
}
|
|
136
|
-
catch (error) {
|
|
137
|
-
console.error(`[CMS] Failed to populate entry ${entry.id} (${entry.slug}):`, error);
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
}));
|
|
141
|
-
return entries.filter((e) => e !== null);
|
|
142
|
-
}
|
|
143
|
-
// Slow path: in-memory pagination (backward compat)
|
|
144
|
-
const ids = options.ids;
|
|
145
|
-
const slug = options.slug;
|
|
146
|
-
const dataValues = options.dataValues;
|
|
147
|
-
const dataLike = options.dataLike;
|
|
148
|
-
const dataILikeOr = options.dataILikeOr;
|
|
149
|
-
const dbEntries = await cms.databaseAdapter.getEntries({
|
|
150
|
-
ids,
|
|
151
|
-
slug,
|
|
152
|
-
orderBy: options.orderBy
|
|
153
|
-
});
|
|
154
|
-
if (dbEntries.length === 0) {
|
|
155
|
-
return [];
|
|
156
|
-
}
|
|
157
|
-
const filteredEntries = status === 'archived'
|
|
158
|
-
? dbEntries.filter((e) => e.archivedAt != null)
|
|
159
|
-
: dbEntries.filter((e) => e.archivedAt == null);
|
|
160
|
-
if (filteredEntries.length === 0) {
|
|
161
|
-
return [];
|
|
162
|
-
}
|
|
163
|
-
const entriesMap = new Map(filteredEntries.map((entry) => [entry.id, entry]));
|
|
164
|
-
const entryIds = filteredEntries.map((entry) => entry.id);
|
|
165
|
-
const allVersions = await cms.databaseAdapter.getEntryVersions({
|
|
166
|
-
entryIds,
|
|
167
|
-
lang: language,
|
|
168
|
-
dataValues,
|
|
169
|
-
dataLike,
|
|
170
|
-
dataILikeOr
|
|
171
|
-
});
|
|
172
|
-
const now = new Date();
|
|
173
|
-
const versionEntries = [];
|
|
174
|
-
const versionsByEntry = new Map();
|
|
175
|
-
for (const v of allVersions) {
|
|
176
|
-
const arr = versionsByEntry.get(v.entryId) || [];
|
|
177
|
-
arr.push(v);
|
|
178
|
-
versionsByEntry.set(v.entryId, arr);
|
|
179
|
-
}
|
|
180
|
-
for (const [entryId, versions] of versionsByEntry) {
|
|
181
|
-
const dbEntry = entriesMap.get(entryId);
|
|
182
|
-
if (!dbEntry)
|
|
183
|
-
continue;
|
|
184
|
-
const sorted = versions.sort((a, b) => b.versionNumber - a.versionNumber);
|
|
185
|
-
let picked = null;
|
|
186
|
-
switch (status) {
|
|
187
|
-
case 'published':
|
|
188
|
-
picked = sorted.find((v) => v.publishedAt != null && v.publishedAt <= now) || null;
|
|
189
|
-
break;
|
|
190
|
-
case 'scheduled':
|
|
191
|
-
picked = sorted.find((v) => v.publishedAt != null && v.publishedAt > now) || null;
|
|
192
|
-
break;
|
|
193
|
-
case 'draft':
|
|
194
|
-
picked = sorted.find((v) => v.publishedAt == null) || null;
|
|
195
|
-
break;
|
|
196
|
-
case 'archived':
|
|
197
|
-
picked = sorted[0] || null;
|
|
198
|
-
break;
|
|
199
|
-
}
|
|
200
|
-
if (picked) {
|
|
201
|
-
versionEntries.push({ version: picked, dbEntry });
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
const entryOrder = new Map(filteredEntries.map((e, i) => [e.id, i]));
|
|
205
|
-
versionEntries.sort((a, b) => (entryOrder.get(a.dbEntry.id) ?? 0) - (entryOrder.get(b.dbEntry.id) ?? 0));
|
|
206
|
-
const entries = await Promise.all(versionEntries.map(async ({ version, dbEntry }) => {
|
|
207
|
-
try {
|
|
208
|
-
const config = cms.getBySlug(dbEntry.slug);
|
|
209
|
-
const fields = getFieldsFromConfig(config);
|
|
210
|
-
const populatedData = await populateEntryData(version.data, fields, language, dbEntry.id);
|
|
211
|
-
const slugPath = getEntrySlugPath(dbEntry.slug);
|
|
212
|
-
const slug = getSlugFromEntryData(version.data, slugPath, language);
|
|
213
|
-
const _url = slug ? getEntryPath(dbEntry.slug, slug) : undefined;
|
|
214
|
-
const result = {
|
|
215
|
-
_id: dbEntry.id,
|
|
216
|
-
_slug: dbEntry.slug,
|
|
217
|
-
_type: dbEntry.type,
|
|
218
|
-
_publishedAt: version.publishedAt,
|
|
219
|
-
_url,
|
|
220
|
-
...populatedData
|
|
221
|
-
};
|
|
222
|
-
if (config.type === 'collection' && config.orderable) {
|
|
223
|
-
result._sortOrder = dbEntry.sortOrder;
|
|
224
|
-
}
|
|
225
|
-
return result;
|
|
226
|
-
}
|
|
227
|
-
catch (error) {
|
|
228
|
-
console.error(`[CMS] Failed to populate entry ${dbEntry.id} (${dbEntry.slug}):`, error);
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
}));
|
|
232
|
-
let results = entries.filter((e) => e !== null);
|
|
233
|
-
if (options.offset)
|
|
234
|
-
results = results.slice(options.offset);
|
|
235
|
-
if (options.limit)
|
|
236
|
-
results = results.slice(0, options.limit);
|
|
237
|
-
return results;
|
|
238
|
-
};
|
|
239
|
-
export const countEntries = async (options) => {
|
|
240
|
-
const cms = getCMS();
|
|
241
|
-
const language = options.language || cms.languages[0];
|
|
242
|
-
const status = options.status || 'published';
|
|
243
|
-
if (cms.databaseAdapter.countPaginatedEntries) {
|
|
244
|
-
return cms.databaseAdapter.countPaginatedEntries({
|
|
245
|
-
slug: options.slug,
|
|
246
|
-
ids: options.ids,
|
|
247
|
-
language,
|
|
248
|
-
status,
|
|
249
|
-
dataValues: options.dataValues,
|
|
250
|
-
dataLike: options.dataLike,
|
|
251
|
-
dataILikeOr: options.dataILikeOr
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
// Fallback: use old getEntries without limit/offset (counts all)
|
|
255
|
-
const entries = await getEntries({ ...options });
|
|
256
|
-
return entries.length;
|
|
257
|
-
};
|
|
258
|
-
export const getEntry = async (options = {}) => {
|
|
259
|
-
const [entry] = await getEntries({
|
|
260
|
-
...options,
|
|
261
|
-
ids: options.id ? [options.id] : undefined
|
|
262
|
-
});
|
|
263
|
-
return entry || null;
|
|
264
|
-
};
|
|
265
|
-
export const getEntryOrThrow = async (options = {}) => {
|
|
266
|
-
const entry = await getEntry(options);
|
|
84
|
+
export const _getRawEntryOrThrow = async (options) => {
|
|
85
|
+
const entry = await _getRawEntry(options);
|
|
267
86
|
if (!entry) {
|
|
268
87
|
throw new Error('Entry not found');
|
|
269
88
|
}
|
|
270
89
|
return entry;
|
|
271
90
|
};
|
|
91
|
+
/**
|
|
92
|
+
* Admin helper: returns dropdown labels for collection entries.
|
|
93
|
+
* Not part of the public resolver API — admin UI use only.
|
|
94
|
+
*/
|
|
272
95
|
export const getEntryLabels = async (options) => {
|
|
273
96
|
const cms = getCMS();
|
|
274
97
|
const config = cms.getBySlug(options.slug);
|
|
275
98
|
if (!config || config.type !== 'collection')
|
|
276
99
|
return [];
|
|
277
|
-
const dbEntries = await
|
|
100
|
+
const dbEntries = await _getDbEntries({
|
|
278
101
|
slug: options.slug,
|
|
279
102
|
ids: options.ids
|
|
280
103
|
});
|
|
@@ -282,7 +105,6 @@ export const getEntryLabels = async (options) => {
|
|
|
282
105
|
return [];
|
|
283
106
|
const entryIds = dbEntries.map((e) => e.id);
|
|
284
107
|
const language = cms.languages[0];
|
|
285
|
-
// Get versions for default language
|
|
286
108
|
const allVersions = await cms.databaseAdapter.getEntryVersions({
|
|
287
109
|
entryIds,
|
|
288
110
|
lang: language
|
|
@@ -290,13 +112,12 @@ export const getEntryLabels = async (options) => {
|
|
|
290
112
|
const now = new Date();
|
|
291
113
|
const statusFilter = options.status ?? 'all';
|
|
292
114
|
const entryAdminTitle = config.entryAdminTitle;
|
|
293
|
-
let results = dbEntries
|
|
115
|
+
let results = dbEntries
|
|
116
|
+
.map((entry) => {
|
|
294
117
|
const entryVersions = allVersions.filter((v) => v.entryId === entry.id);
|
|
295
118
|
const sorted = entryVersions.sort((a, b) => b.versionNumber - a.versionNumber);
|
|
296
|
-
// Determine status from versions
|
|
297
119
|
const publishedVersion = sorted.find((v) => v.publishedAt != null && v.publishedAt <= now) || null;
|
|
298
120
|
const hasPublished = publishedVersion != null;
|
|
299
|
-
// Filter by status
|
|
300
121
|
if (statusFilter === 'published' && !hasPublished)
|
|
301
122
|
return null;
|
|
302
123
|
if (statusFilter === 'draft' && hasPublished)
|
|
@@ -310,50 +131,60 @@ export const getEntryLabels = async (options) => {
|
|
|
310
131
|
}
|
|
311
132
|
}
|
|
312
133
|
return { id: entry.id, label };
|
|
313
|
-
})
|
|
314
|
-
|
|
134
|
+
})
|
|
135
|
+
.filter((r) => r != null);
|
|
315
136
|
if (options.search) {
|
|
316
137
|
const searchLower = options.search.toLowerCase();
|
|
317
138
|
results = results.filter((r) => r.label.toLowerCase().includes(searchLower));
|
|
318
139
|
}
|
|
319
140
|
const total = results.length;
|
|
320
|
-
// Apply limit
|
|
321
141
|
if (options.limit && options.limit > 0) {
|
|
322
142
|
results = results.slice(0, options.limit);
|
|
323
143
|
}
|
|
324
144
|
return results.map((r) => ({ ...r, total }));
|
|
325
145
|
};
|
|
326
|
-
export const
|
|
146
|
+
export const _getDbEntryVersions = async (options) => {
|
|
327
147
|
return getCMS().databaseAdapter.getEntryVersions(options);
|
|
328
148
|
};
|
|
329
|
-
export const
|
|
330
|
-
const [version] = await
|
|
149
|
+
export const _getDbEntryVersion = async (options) => {
|
|
150
|
+
const [version] = await _getDbEntryVersions({
|
|
331
151
|
...options,
|
|
332
152
|
ids: options.id ? [options.id] : undefined
|
|
333
153
|
});
|
|
334
154
|
return version || null;
|
|
335
155
|
};
|
|
336
|
-
export const
|
|
337
|
-
const version = await
|
|
156
|
+
export const _getDbEntryVersionOrThrow = async (options) => {
|
|
157
|
+
const version = await _getDbEntryVersion(options);
|
|
338
158
|
if (!version) {
|
|
339
159
|
throw new Error('Entry version not found');
|
|
340
160
|
}
|
|
341
161
|
return version;
|
|
342
162
|
};
|
|
163
|
+
/**
|
|
164
|
+
* Admin helper: fetches a specific version of an entry by version id and populates it.
|
|
165
|
+
* Used by version-history admin UI; not part of public resolver API.
|
|
166
|
+
*/
|
|
343
167
|
export const getEntryVersion = async (options) => {
|
|
344
168
|
const language = options.language || getCMS().languages[0];
|
|
345
|
-
const dbEntryVersion = await
|
|
346
|
-
if (!dbEntryVersion)
|
|
169
|
+
const dbEntryVersion = await _getDbEntryVersion({ id: options.id });
|
|
170
|
+
if (!dbEntryVersion)
|
|
347
171
|
return null;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
if (!dbEntry) {
|
|
172
|
+
const dbEntry = await _getDbEntry({ id: dbEntryVersion.entryId });
|
|
173
|
+
if (!dbEntry)
|
|
351
174
|
return null;
|
|
352
|
-
}
|
|
353
175
|
try {
|
|
354
176
|
const config = getCMS().getBySlug(dbEntry.slug);
|
|
355
177
|
const fields = getFieldsFromConfig(config);
|
|
356
|
-
const
|
|
178
|
+
const { _populate } = await import('../../fields/populateEntry.js');
|
|
179
|
+
const populatedData = await _populate(dbEntryVersion.data, fields, {
|
|
180
|
+
locale: language,
|
|
181
|
+
status: 'published',
|
|
182
|
+
depth: 0,
|
|
183
|
+
maxDepth: 5,
|
|
184
|
+
visited: new Set([dbEntry.id]),
|
|
185
|
+
populate: {},
|
|
186
|
+
entryId: dbEntry.id
|
|
187
|
+
});
|
|
357
188
|
const slugPath = getEntrySlugPath(dbEntry.slug);
|
|
358
189
|
const entrySlug = getSlugFromEntryData(dbEntryVersion.data, slugPath, language);
|
|
359
190
|
const _url = entrySlug ? getEntryPath(dbEntry.slug, entrySlug) : undefined;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Entry } from '../../../../types/entries.js';
|
|
2
|
+
/**
|
|
3
|
+
* Status filter for `resolveEntry`/`resolveEntries`/`countEntries`.
|
|
4
|
+
* - `published` (default): newest version with `publishedAt <= now`
|
|
5
|
+
* - `draft`: newest version without `publishedAt`
|
|
6
|
+
* - `scheduled`: newest version with `publishedAt > now` (use case: countdown to launch)
|
|
7
|
+
*
|
|
8
|
+
* `archived` is intentionally excluded from public API (admin uses internal `_getRawEntries`).
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
export type ResolveStatus = 'published' | 'draft' | 'scheduled';
|
|
12
|
+
/**
|
|
13
|
+
* Recursion + per-field opt-out config for relation population.
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
export type PopulateConfig = {
|
|
17
|
+
/** Hard cap on relation depth. Default 5. Use `0` to keep all relations as raw IDs. */
|
|
18
|
+
maxDepth?: number;
|
|
19
|
+
/** Per-field opt-out. `{ author: false }` keeps `author` as raw ID instead of populated Entry. */
|
|
20
|
+
fields?: Record<string, false>;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Internal context passed through the populate chain.
|
|
24
|
+
* Carries locale/status cascade, recursion guards, and the current entry id (for shop populate).
|
|
25
|
+
*/
|
|
26
|
+
export interface PopulateCtx {
|
|
27
|
+
locale: string;
|
|
28
|
+
status: ResolveStatus;
|
|
29
|
+
depth: number;
|
|
30
|
+
maxDepth: number;
|
|
31
|
+
visited: Set<string>;
|
|
32
|
+
populate: PopulateConfig;
|
|
33
|
+
entryId: string;
|
|
34
|
+
}
|
|
35
|
+
/** @public */
|
|
36
|
+
export interface ResolveEntryOptions {
|
|
37
|
+
id?: string;
|
|
38
|
+
collection?: string;
|
|
39
|
+
locale?: string;
|
|
40
|
+
status?: ResolveStatus;
|
|
41
|
+
populate?: PopulateConfig;
|
|
42
|
+
}
|
|
43
|
+
/** @public */
|
|
44
|
+
export interface ResolveEntriesOptions {
|
|
45
|
+
collection: string;
|
|
46
|
+
locale?: string;
|
|
47
|
+
status?: ResolveStatus;
|
|
48
|
+
ids?: string[];
|
|
49
|
+
filter?: {
|
|
50
|
+
dataValues?: Record<string, unknown>;
|
|
51
|
+
dataLike?: Record<string, unknown>;
|
|
52
|
+
dataILikeOr?: Record<string, unknown>;
|
|
53
|
+
};
|
|
54
|
+
orderBy?: {
|
|
55
|
+
column: 'createdAt' | 'updatedAt' | 'sortOrder';
|
|
56
|
+
direction: 'asc' | 'desc';
|
|
57
|
+
};
|
|
58
|
+
dataOrderBy?: {
|
|
59
|
+
field: string;
|
|
60
|
+
direction: 'asc' | 'desc';
|
|
61
|
+
};
|
|
62
|
+
limit?: number;
|
|
63
|
+
offset?: number;
|
|
64
|
+
populate?: PopulateConfig;
|
|
65
|
+
}
|
|
66
|
+
/** @public */
|
|
67
|
+
export type CountEntriesOptions = Omit<ResolveEntriesOptions, 'limit' | 'offset' | 'populate' | 'orderBy' | 'dataOrderBy'>;
|
|
68
|
+
/**
|
|
69
|
+
* Fetch a single populated Entry.
|
|
70
|
+
*
|
|
71
|
+
* - At least one of `id` or `collection` must be provided.
|
|
72
|
+
* - `locale` defaults to `cms.languages[0]`. Strict — returns `null` when no version exists in the requested locale.
|
|
73
|
+
* - `status` defaults to `'published'`. See {@link ResolveStatus}.
|
|
74
|
+
* - `populate` controls relation depth + per-field opt-out. See {@link PopulateConfig}.
|
|
75
|
+
*
|
|
76
|
+
* @public
|
|
77
|
+
*/
|
|
78
|
+
export declare function resolveEntry(opts: ResolveEntryOptions): Promise<Entry | null>;
|
|
79
|
+
/**
|
|
80
|
+
* Fetch a list of populated Entries from a collection (or singleton with multiple instances).
|
|
81
|
+
*
|
|
82
|
+
* - `collection` is required.
|
|
83
|
+
* - `locale` strict (excludes entries without a version in requested locale).
|
|
84
|
+
* - `filter.{dataValues, dataLike, dataILikeOr}` map onto adapter `getEntryVersions` filters.
|
|
85
|
+
* - `limit`/`offset` applied after status pick.
|
|
86
|
+
*
|
|
87
|
+
* @public
|
|
88
|
+
*/
|
|
89
|
+
export declare function resolveEntries(opts: ResolveEntriesOptions): Promise<Entry[]>;
|
|
90
|
+
/**
|
|
91
|
+
* Count entries matching the same filters as `resolveEntries`, without populating.
|
|
92
|
+
* @public
|
|
93
|
+
*/
|
|
94
|
+
export declare function countEntries(opts: CountEntriesOptions): Promise<number>;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { getCMS } from '../../../cms.js';
|
|
2
|
+
import { getFieldsFromConfig } from '../../../fields/layoutUtils.js';
|
|
3
|
+
import { getEntrySlugPath, getSlugFromEntryData, getEntryPath } from '../../fields/slugResolver.js';
|
|
4
|
+
function pickVersion(versions, status) {
|
|
5
|
+
const sorted = versions.slice().sort((a, b) => b.versionNumber - a.versionNumber);
|
|
6
|
+
const now = new Date();
|
|
7
|
+
switch (status) {
|
|
8
|
+
case 'published':
|
|
9
|
+
return sorted.find((v) => v.publishedAt != null && v.publishedAt <= now) ?? null;
|
|
10
|
+
case 'draft':
|
|
11
|
+
return sorted.find((v) => v.publishedAt == null) ?? null;
|
|
12
|
+
case 'scheduled':
|
|
13
|
+
return sorted.find((v) => v.publishedAt != null && v.publishedAt > now) ?? null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function buildEntry(dbEntry, version, ctx) {
|
|
17
|
+
const cms = getCMS();
|
|
18
|
+
let config;
|
|
19
|
+
try {
|
|
20
|
+
config = cms.getBySlug(dbEntry.slug);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Orphaned entry — slug removed from config
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const fields = getFieldsFromConfig(config);
|
|
27
|
+
// Lazy import to avoid static circular dep with resolveRelationFields → _populate
|
|
28
|
+
const { _populate } = await import('../../fields/populateEntry.js');
|
|
29
|
+
const populated = await _populate(version.data, fields, ctx);
|
|
30
|
+
const slugPath = getEntrySlugPath(dbEntry.slug);
|
|
31
|
+
const slugValue = getSlugFromEntryData(version.data, slugPath, ctx.locale);
|
|
32
|
+
const _url = slugValue ? getEntryPath(dbEntry.slug, slugValue) : undefined;
|
|
33
|
+
const result = {
|
|
34
|
+
_id: dbEntry.id,
|
|
35
|
+
_slug: dbEntry.slug,
|
|
36
|
+
_type: dbEntry.type,
|
|
37
|
+
_publishedAt: version.publishedAt,
|
|
38
|
+
_url,
|
|
39
|
+
...populated
|
|
40
|
+
};
|
|
41
|
+
if (config.type === 'collection' && config.orderable) {
|
|
42
|
+
result._sortOrder = dbEntry.sortOrder;
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Fetch a single populated Entry.
|
|
48
|
+
*
|
|
49
|
+
* - At least one of `id` or `collection` must be provided.
|
|
50
|
+
* - `locale` defaults to `cms.languages[0]`. Strict — returns `null` when no version exists in the requested locale.
|
|
51
|
+
* - `status` defaults to `'published'`. See {@link ResolveStatus}.
|
|
52
|
+
* - `populate` controls relation depth + per-field opt-out. See {@link PopulateConfig}.
|
|
53
|
+
*
|
|
54
|
+
* @public
|
|
55
|
+
*/
|
|
56
|
+
export async function resolveEntry(opts) {
|
|
57
|
+
if (!opts.id && !opts.collection) {
|
|
58
|
+
throw new Error('resolveEntry: must provide id or collection');
|
|
59
|
+
}
|
|
60
|
+
const cms = getCMS();
|
|
61
|
+
const locale = opts.locale ?? cms.languages[0];
|
|
62
|
+
const status = opts.status ?? 'published';
|
|
63
|
+
const populate = opts.populate ?? {};
|
|
64
|
+
const maxDepth = populate.maxDepth ?? 5;
|
|
65
|
+
const dbOpts = {};
|
|
66
|
+
if (opts.id)
|
|
67
|
+
dbOpts.ids = [opts.id];
|
|
68
|
+
if (opts.collection)
|
|
69
|
+
dbOpts.slug = opts.collection;
|
|
70
|
+
const dbEntries = await cms.databaseAdapter.getEntries(dbOpts);
|
|
71
|
+
const dbEntry = dbEntries.find((e) => e.archivedAt == null) ?? null;
|
|
72
|
+
if (!dbEntry)
|
|
73
|
+
return null;
|
|
74
|
+
if (opts.collection && dbEntry.slug !== opts.collection)
|
|
75
|
+
return null;
|
|
76
|
+
const versions = await cms.databaseAdapter.getEntryVersions({
|
|
77
|
+
entryIds: [dbEntry.id],
|
|
78
|
+
lang: locale
|
|
79
|
+
});
|
|
80
|
+
const picked = pickVersion(versions, status);
|
|
81
|
+
if (!picked)
|
|
82
|
+
return null;
|
|
83
|
+
const ctx = {
|
|
84
|
+
locale,
|
|
85
|
+
status,
|
|
86
|
+
depth: 0,
|
|
87
|
+
maxDepth,
|
|
88
|
+
visited: new Set([dbEntry.id]),
|
|
89
|
+
populate,
|
|
90
|
+
entryId: dbEntry.id
|
|
91
|
+
};
|
|
92
|
+
return buildEntry(dbEntry, picked, ctx);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Fetch a list of populated Entries from a collection (or singleton with multiple instances).
|
|
96
|
+
*
|
|
97
|
+
* - `collection` is required.
|
|
98
|
+
* - `locale` strict (excludes entries without a version in requested locale).
|
|
99
|
+
* - `filter.{dataValues, dataLike, dataILikeOr}` map onto adapter `getEntryVersions` filters.
|
|
100
|
+
* - `limit`/`offset` applied after status pick.
|
|
101
|
+
*
|
|
102
|
+
* @public
|
|
103
|
+
*/
|
|
104
|
+
export async function resolveEntries(opts) {
|
|
105
|
+
if (!opts.collection) {
|
|
106
|
+
throw new Error('resolveEntries: collection is required');
|
|
107
|
+
}
|
|
108
|
+
const cms = getCMS();
|
|
109
|
+
const locale = opts.locale ?? cms.languages[0];
|
|
110
|
+
const status = opts.status ?? 'published';
|
|
111
|
+
const populate = opts.populate ?? {};
|
|
112
|
+
const maxDepth = populate.maxDepth ?? 5;
|
|
113
|
+
const dbEntries = await cms.databaseAdapter.getEntries({
|
|
114
|
+
ids: opts.ids,
|
|
115
|
+
slug: opts.collection,
|
|
116
|
+
orderBy: opts.orderBy
|
|
117
|
+
});
|
|
118
|
+
if (dbEntries.length === 0)
|
|
119
|
+
return [];
|
|
120
|
+
const filtered = dbEntries.filter((e) => e.archivedAt == null);
|
|
121
|
+
if (filtered.length === 0)
|
|
122
|
+
return [];
|
|
123
|
+
const entryIds = filtered.map((e) => e.id);
|
|
124
|
+
const allVersions = await cms.databaseAdapter.getEntryVersions({
|
|
125
|
+
entryIds,
|
|
126
|
+
lang: locale,
|
|
127
|
+
dataValues: opts.filter?.dataValues,
|
|
128
|
+
dataLike: opts.filter?.dataLike,
|
|
129
|
+
dataILikeOr: opts.filter?.dataILikeOr
|
|
130
|
+
});
|
|
131
|
+
const versionsByEntry = new Map();
|
|
132
|
+
for (const v of allVersions) {
|
|
133
|
+
const arr = versionsByEntry.get(v.entryId) ?? [];
|
|
134
|
+
arr.push(v);
|
|
135
|
+
versionsByEntry.set(v.entryId, arr);
|
|
136
|
+
}
|
|
137
|
+
const order = new Map(filtered.map((e, i) => [e.id, i]));
|
|
138
|
+
const picks = [];
|
|
139
|
+
for (const dbEntry of filtered) {
|
|
140
|
+
const vs = versionsByEntry.get(dbEntry.id) ?? [];
|
|
141
|
+
const picked = pickVersion(vs, status);
|
|
142
|
+
if (picked)
|
|
143
|
+
picks.push({ version: picked, dbEntry });
|
|
144
|
+
}
|
|
145
|
+
picks.sort((a, b) => (order.get(a.dbEntry.id) ?? 0) - (order.get(b.dbEntry.id) ?? 0));
|
|
146
|
+
const built = await Promise.all(picks.map(async ({ version, dbEntry }) => {
|
|
147
|
+
const ctx = {
|
|
148
|
+
locale,
|
|
149
|
+
status,
|
|
150
|
+
depth: 0,
|
|
151
|
+
maxDepth,
|
|
152
|
+
visited: new Set([dbEntry.id]),
|
|
153
|
+
populate,
|
|
154
|
+
entryId: dbEntry.id
|
|
155
|
+
};
|
|
156
|
+
try {
|
|
157
|
+
return await buildEntry(dbEntry, version, ctx);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
console.error(`[CMS] Failed to populate entry ${dbEntry.id} (${dbEntry.slug}):`, error);
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}));
|
|
164
|
+
let results = built.filter((e) => e !== null);
|
|
165
|
+
if (opts.offset)
|
|
166
|
+
results = results.slice(opts.offset);
|
|
167
|
+
if (opts.limit)
|
|
168
|
+
results = results.slice(0, opts.limit);
|
|
169
|
+
return results;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Count entries matching the same filters as `resolveEntries`, without populating.
|
|
173
|
+
* @public
|
|
174
|
+
*/
|
|
175
|
+
export async function countEntries(opts) {
|
|
176
|
+
if (!opts.collection) {
|
|
177
|
+
throw new Error('countEntries: collection is required');
|
|
178
|
+
}
|
|
179
|
+
const cms = getCMS();
|
|
180
|
+
const locale = opts.locale ?? cms.languages[0];
|
|
181
|
+
const status = opts.status ?? 'published';
|
|
182
|
+
const dbEntries = await cms.databaseAdapter.getEntries({
|
|
183
|
+
ids: opts.ids,
|
|
184
|
+
slug: opts.collection
|
|
185
|
+
});
|
|
186
|
+
const filtered = dbEntries.filter((e) => e.archivedAt == null);
|
|
187
|
+
if (filtered.length === 0)
|
|
188
|
+
return 0;
|
|
189
|
+
const entryIds = filtered.map((e) => e.id);
|
|
190
|
+
const allVersions = await cms.databaseAdapter.getEntryVersions({
|
|
191
|
+
entryIds,
|
|
192
|
+
lang: locale,
|
|
193
|
+
dataValues: opts.filter?.dataValues,
|
|
194
|
+
dataLike: opts.filter?.dataLike,
|
|
195
|
+
dataILikeOr: opts.filter?.dataILikeOr
|
|
196
|
+
});
|
|
197
|
+
const versionsByEntry = new Map();
|
|
198
|
+
for (const v of allVersions) {
|
|
199
|
+
const arr = versionsByEntry.get(v.entryId) ?? [];
|
|
200
|
+
arr.push(v);
|
|
201
|
+
versionsByEntry.set(v.entryId, arr);
|
|
202
|
+
}
|
|
203
|
+
let count = 0;
|
|
204
|
+
for (const dbEntry of filtered) {
|
|
205
|
+
const vs = versionsByEntry.get(dbEntry.id) ?? [];
|
|
206
|
+
if (pickVersion(vs, status))
|
|
207
|
+
count++;
|
|
208
|
+
}
|
|
209
|
+
return count;
|
|
210
|
+
}
|
|
@@ -2,7 +2,7 @@ import { getCMS } from '../../../cms.js';
|
|
|
2
2
|
import { generateZodSchemaFromFields } from '../../../fields/fieldSchemaToTs.js';
|
|
3
3
|
import { getFieldsFromConfig } from '../../../fields/layoutUtils.js';
|
|
4
4
|
import z from 'zod';
|
|
5
|
-
import { getDbEntryOrThrow, getDbEntryVersionOrThrow, getDbEntryVersions } from './get.js';
|
|
5
|
+
import { _getDbEntryOrThrow as getDbEntryOrThrow, _getDbEntryVersionOrThrow as getDbEntryVersionOrThrow, _getDbEntryVersions as getDbEntryVersions } from './get.js';
|
|
6
6
|
import { createEntryVersion } from './create.js';
|
|
7
7
|
export const updateEntrySchema = z.object({
|
|
8
8
|
archivedAt: z.date().nullable().optional(),
|