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.
Files changed (52) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/DOCS.md +1 -1
  3. package/dist/admin/api/rest/routes/collections.js +1 -1
  4. package/dist/admin/api/rest/routes/entries.js +1 -1
  5. package/dist/admin/api/rest/routes/singletons.js +1 -1
  6. package/dist/admin/remote/entry.remote.js +20 -3
  7. package/dist/admin/remote/invite.d.ts +1 -1
  8. package/dist/admin/remote/preview.remote.js +10 -2
  9. package/dist/admin/remote/reorder.js +1 -1
  10. package/dist/admin/remote/shop.remote.d.ts +18 -18
  11. package/dist/cms/runtime/api.d.ts +10 -6
  12. package/dist/cms/runtime/api.js +7 -7
  13. package/dist/components/ui/accordion/accordion.svelte.d.ts +1 -1
  14. package/dist/components/ui/calendar/calendar.svelte.d.ts +1 -1
  15. package/dist/components/ui/command/command-dialog.svelte.d.ts +1 -1
  16. package/dist/components/ui/command/command-input.svelte.d.ts +1 -1
  17. package/dist/components/ui/command/command.svelte.d.ts +1 -1
  18. package/dist/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte.d.ts +1 -1
  19. package/dist/components/ui/input/input.svelte.d.ts +1 -1
  20. package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
  21. package/dist/components/ui/input-group/input-group-textarea.svelte.d.ts +1 -1
  22. package/dist/components/ui/radio-group/radio-group.svelte.d.ts +1 -1
  23. package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
  24. package/dist/components/ui/tabs/tabs.svelte.d.ts +1 -1
  25. package/dist/components/ui/textarea/textarea.svelte.d.ts +1 -1
  26. package/dist/components/ui/toggle-group/toggle-group-item.svelte.d.ts +1 -1
  27. package/dist/components/ui/toggle-group/toggle-group.svelte.d.ts +1 -1
  28. package/dist/core/server/entries/operations/create.js +1 -1
  29. package/dist/core/server/entries/operations/get.d.ts +20 -16
  30. package/dist/core/server/entries/operations/get.js +45 -214
  31. package/dist/core/server/entries/operations/resolveEntry.d.ts +94 -0
  32. package/dist/core/server/entries/operations/resolveEntry.js +210 -0
  33. package/dist/core/server/entries/operations/update.js +1 -1
  34. package/dist/core/server/fields/populateEntry.d.ts +9 -1
  35. package/dist/core/server/fields/populateEntry.js +22 -18
  36. package/dist/core/server/fields/resolveRelationFields.d.ts +2 -1
  37. package/dist/core/server/fields/resolveRelationFields.js +140 -34
  38. package/dist/core/server/fields/resolveRichtextLinks.d.ts +2 -1
  39. package/dist/core/server/fields/resolveRichtextLinks.js +2 -1
  40. package/dist/core/server/fields/resolveUrlFields.d.ts +2 -1
  41. package/dist/core/server/fields/resolveUrlFields.js +6 -5
  42. package/dist/core/server/generator/generator.js +17 -14
  43. package/dist/entity/index.js +1 -1
  44. package/dist/shop/server/populate.d.ts +2 -1
  45. package/dist/shop/server/populate.js +2 -1
  46. package/dist/sveltekit/server/index.d.ts +1 -1
  47. package/dist/sveltekit/server/index.js +1 -1
  48. package/dist/types/plugins.d.ts +6 -2
  49. package/dist/updates/0.17.0/index.d.ts +2 -0
  50. package/dist/updates/0.17.0/index.js +78 -0
  51. package/dist/updates/index.js +2 -1
  52. 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 getDbEntries = async (options) => {
5
+ export const _getDbEntries = async (options) => {
7
6
  return getCMS().databaseAdapter.getEntries(options);
8
7
  };
9
- export const countDbEntries = async (options) => {
8
+ export const _countDbEntries = async (options) => {
10
9
  return getCMS().databaseAdapter.countEntries(options);
11
10
  };
12
- export const getDbEntry = async (options) => {
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 getDbEntryOrThrow = async (options) => {
21
- const entry = await getDbEntry(options);
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 countRawEntries = async (options) => {
28
- return countDbEntries(options);
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 getRawEntries = async (options) => {
58
- const dbEntries = await getDbEntries(options);
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 getRawEntry = async (options) => {
82
- const [entry] = await getRawEntries({
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 getRawEntryOrThrow = async (options) => {
90
- const entry = await getRawEntry(options);
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 getDbEntries({
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.map((entry) => {
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
- }).filter((r) => r != null);
314
- // Post-query search filtering (case-insensitive includes)
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 getDbEntryVersions = async (options) => {
146
+ export const _getDbEntryVersions = async (options) => {
327
147
  return getCMS().databaseAdapter.getEntryVersions(options);
328
148
  };
329
- export const getDbEntryVersion = async (options) => {
330
- const [version] = await getDbEntryVersions({
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 getDbEntryVersionOrThrow = async (options) => {
337
- const version = await getDbEntryVersion(options);
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 getDbEntryVersion({ id: options.id });
346
- if (!dbEntryVersion) {
169
+ const dbEntryVersion = await _getDbEntryVersion({ id: options.id });
170
+ if (!dbEntryVersion)
347
171
  return null;
348
- }
349
- const dbEntry = await getDbEntry({ id: dbEntryVersion.entryId });
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 populatedData = await populateEntryData(dbEntryVersion.data, fields, language, dbEntry.id);
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(),