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,3 +1,11 @@
1
1
  import type { EntryData, PopulatedEntryData } from '../../../types/entries.js';
2
2
  import type { Field } from '../../../types/fields.js';
3
- export declare function populateEntryData(data: EntryData, fields: Field[], language: string, entryId?: string): Promise<PopulatedEntryData>;
3
+ import type { PopulateCtx } from '../entries/operations/resolveEntry.js';
4
+ /**
5
+ * Internal populate chain: relations → urls → media → richtext → custom → shop → typography orphans.
6
+ * Carries `ctx` (locale, status, depth, visited, populate config, current entryId) so every nested
7
+ * field resolver can cascade locale + recursion guards consistently.
8
+ *
9
+ * Not part of the public API. Userland uses `resolveEntry`/`resolveEntries`.
10
+ */
11
+ export declare function _populate(data: EntryData, fields: Field[], ctx: PopulateCtx): Promise<PopulatedEntryData>;
@@ -5,11 +5,8 @@ import { resolveUrlFields } from './resolveUrlFields.js';
5
5
  import { resolveTypographyOrphans } from './resolveTypographyOrphans.js';
6
6
  import { getCMS } from '../../cms.js';
7
7
  import { resolveShopFields } from '../../../shop/server/populate.js';
8
- async function resolveCustomFields(data, fields) {
9
- // Check if any custom fields exist before accessing CMS
10
- const hasCustom = fields.some((f) => f.type === 'custom' ||
11
- f.type === 'object' ||
12
- f.type === 'blocks');
8
+ async function resolveCustomFields(data, fields, ctx) {
9
+ const hasCustom = fields.some((f) => f.type === 'custom' || f.type === 'object' || f.type === 'blocks');
13
10
  if (!hasCustom)
14
11
  return data;
15
12
  let cms;
@@ -26,13 +23,14 @@ async function resolveCustomFields(data, fields) {
26
23
  case 'custom': {
27
24
  const def = cms.customFields.get(field.fieldType);
28
25
  if (def?.populateResolver && val != null) {
29
- result[field.slug] = await def.populateResolver(val, field);
26
+ // Plugin populateResolver receives ctx as 3rd arg (breaking in 0.17.0; plugin API @experimental)
27
+ result[field.slug] = await def.populateResolver(val, field, ctx);
30
28
  }
31
29
  break;
32
30
  }
33
31
  case 'object':
34
32
  if (val && typeof val === 'object') {
35
- result[field.slug] = await resolveCustomFields(val, field.fields);
33
+ result[field.slug] = await resolveCustomFields(val, field.fields, ctx);
36
34
  }
37
35
  break;
38
36
  case 'blocks':
@@ -40,7 +38,7 @@ async function resolveCustomFields(data, fields) {
40
38
  result[field.slug] = await Promise.all(val.map(async (item) => {
41
39
  const blockDef = field.of.find((d) => d.slug === item._slug);
42
40
  if (blockDef) {
43
- return await resolveCustomFields(item, blockDef.fields);
41
+ return await resolveCustomFields(item, blockDef.fields, ctx);
44
42
  }
45
43
  return item;
46
44
  }));
@@ -50,14 +48,20 @@ async function resolveCustomFields(data, fields) {
50
48
  }
51
49
  return result;
52
50
  }
53
- export async function populateEntryData(data, fields, language, entryId) {
54
- let populatedData = await resolveRelationFields(data, fields, language);
55
- populatedData = await resolveUrlFields(populatedData, fields, language);
56
- populatedData = await resolveMediaFields(populatedData, fields);
57
- populatedData = await resolveRichtextLinks(populatedData, fields, language);
58
- populatedData = (await resolveCustomFields(populatedData, fields));
59
- populatedData = (await resolveShopFields(populatedData, fields, entryId));
60
- // Typography orphan fix — enabled by default, opt-out via config
51
+ /**
52
+ * Internal populate chain: relations → urls → media → richtext → custom → shop → typography orphans.
53
+ * Carries `ctx` (locale, status, depth, visited, populate config, current entryId) so every nested
54
+ * field resolver can cascade locale + recursion guards consistently.
55
+ *
56
+ * Not part of the public API. Userland uses `resolveEntry`/`resolveEntries`.
57
+ */
58
+ export async function _populate(data, fields, ctx) {
59
+ let populated = await resolveRelationFields(data, fields, ctx);
60
+ populated = await resolveUrlFields(populated, fields, ctx);
61
+ populated = await resolveMediaFields(populated, fields);
62
+ populated = await resolveRichtextLinks(populated, fields, ctx);
63
+ populated = (await resolveCustomFields(populated, fields, ctx));
64
+ populated = (await resolveShopFields(populated, fields, ctx));
61
65
  let fixOrphans = true;
62
66
  try {
63
67
  fixOrphans = getCMS().typographyConfig.fixOrphans !== false;
@@ -66,7 +70,7 @@ export async function populateEntryData(data, fields, language, entryId) {
66
70
  // CMS not initialized — keep default
67
71
  }
68
72
  if (fixOrphans) {
69
- populatedData = resolveTypographyOrphans(populatedData, fields);
73
+ populated = resolveTypographyOrphans(populated, fields);
70
74
  }
71
- return populatedData;
75
+ return populated;
72
76
  }
@@ -1,3 +1,4 @@
1
1
  import type { EntryData, PopulatedEntryData } from '../../../types/entries.js';
2
2
  import type { Field } from '../../../types/fields.js';
3
- export declare function resolveRelationFields(data: EntryData, fields: Field[], language: string): Promise<PopulatedEntryData>;
3
+ import type { PopulateCtx } from '../entries/operations/resolveEntry.js';
4
+ export declare function resolveRelationFields(data: EntryData, fields: Field[], ctx: PopulateCtx): Promise<PopulatedEntryData>;
@@ -1,53 +1,72 @@
1
1
  import { walkInlineBlockNodes, cloneDoc } from '../../../admin/components/tiptap/structured-content-utils.js';
2
- import z from 'zod';
3
- export async function resolveRelationFields(data, fields, language) {
2
+ import { getCMS } from '../../cms.js';
3
+ import { getFieldsFromConfig } from '../../fields/layoutUtils.js';
4
+ import { getEntrySlugPath, getSlugFromEntryData, getEntryPath } from './slugResolver.js';
5
+ function pickVersion(versions, status) {
6
+ const sorted = versions.slice().sort((a, b) => b.versionNumber - a.versionNumber);
7
+ const now = new Date();
8
+ switch (status) {
9
+ case 'published':
10
+ return sorted.find((v) => v.publishedAt != null && v.publishedAt <= now) ?? null;
11
+ case 'draft':
12
+ return sorted.find((v) => v.publishedAt == null) ?? null;
13
+ case 'scheduled':
14
+ return sorted.find((v) => v.publishedAt != null && v.publishedAt > now) ?? null;
15
+ }
16
+ }
17
+ export async function resolveRelationFields(data, fields, ctx) {
4
18
  const entriesIds = [];
19
+ const optedOutFields = new Set();
20
+ // Collect ids skipping fields opted out at top level (raw passthrough).
5
21
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
- const collectIds = (value, fields) => {
22
+ const collectIds = (value, fields, topLevel) => {
7
23
  for (const field of fields) {
8
24
  const val = value?.[field.slug];
9
25
  if (val == null)
10
26
  continue;
27
+ if (topLevel && ctx.populate.fields?.[field.slug] === false) {
28
+ optedOutFields.add(field.slug);
29
+ continue;
30
+ }
11
31
  switch (field.type) {
12
32
  case 'relation': {
13
33
  if (field.multiple && Array.isArray(val)) {
14
- val.forEach((id) => {
15
- if (z.string().uuid().safeParse(id).success) {
34
+ for (const id of val) {
35
+ if (typeof id === 'string')
16
36
  entriesIds.push(id);
17
- }
18
- });
37
+ }
19
38
  }
20
39
  else if (typeof val === 'string') {
21
- if (z.string().uuid().safeParse(val).success) {
22
- entriesIds.push(val);
23
- }
40
+ entriesIds.push(val);
24
41
  }
25
42
  break;
26
43
  }
27
44
  case 'object':
28
- collectIds(val, field.fields);
45
+ collectIds(val, field.fields, false);
29
46
  break;
30
47
  case 'blocks':
31
48
  if (Array.isArray(val)) {
32
49
  val.forEach((item) => {
33
50
  const objectDef = field.of.find((objDef) => objDef.slug === item._slug);
34
51
  if (objectDef) {
35
- collectIds(item, objectDef.fields);
52
+ collectIds(item, objectDef.fields, false);
36
53
  }
37
54
  });
38
55
  }
39
56
  break;
40
57
  case 'content': {
41
58
  const cf = field;
42
- // Content is now a single doc, not Record<lang, doc>
43
- if (val && typeof val === 'object' && val.type === 'doc' && cf.inlineBlocks?.length) {
59
+ if (val &&
60
+ typeof val === 'object' &&
61
+ val.type === 'doc' &&
62
+ cf.inlineBlocks?.length) {
44
63
  walkInlineBlockNodes(val, (node) => {
45
64
  const bd = node.attrs?.blockData;
46
65
  if (!bd || typeof bd !== 'object')
47
66
  return;
48
67
  const def = cf.inlineBlocks.find((b) => b.slug === node.attrs?.blockType);
49
68
  if (def)
50
- collectIds(bd, def.fields);
69
+ collectIds(bd, def.fields, false);
51
70
  });
52
71
  }
53
72
  break;
@@ -55,20 +74,97 @@ export async function resolveRelationFields(data, fields, language) {
55
74
  }
56
75
  }
57
76
  };
58
- collectIds(data, fields);
77
+ collectIds(data, fields, true);
59
78
  if (entriesIds.length === 0)
60
79
  return data;
61
- // Import getEntries dynamically to avoid circular dependency
62
- const { getEntries } = await import('../entries/operations/get.js');
63
- // Get fully populated entries
64
- const entries = await getEntries({
65
- ids: entriesIds,
66
- language
67
- });
68
- const entriesMap = Object.fromEntries(entries.map((e) => [e._id, e]));
80
+ // Filter: don't fetch already-visited (cycle) or beyond maxDepth — those stay raw IDs
81
+ const overDepth = ctx.depth >= ctx.maxDepth;
82
+ const fetchableIds = overDepth
83
+ ? []
84
+ : [...new Set(entriesIds)].filter((id) => !ctx.visited.has(id));
85
+ const cms = getCMS();
86
+ // entriesMap: id → Entry (populated) | null (fetched but missing version/missing entry).
87
+ // Absent key = id was NOT fetched (cycle, max depth, top-level opt-out) → falls back to raw id.
88
+ const entriesMap = {};
89
+ if (fetchableIds.length > 0) {
90
+ const dbEntries = await cms.databaseAdapter.getEntries({ ids: fetchableIds });
91
+ const aliveDbEntries = dbEntries.filter((e) => e.archivedAt == null);
92
+ const aliveIds = new Set(aliveDbEntries.map((e) => e.id));
93
+ // Strict: missing entry / archived → null in nested
94
+ for (const id of fetchableIds) {
95
+ if (!aliveIds.has(id))
96
+ entriesMap[id] = null;
97
+ }
98
+ if (aliveDbEntries.length > 0) {
99
+ const versions = await cms.databaseAdapter.getEntryVersions({
100
+ entryIds: aliveDbEntries.map((e) => e.id),
101
+ lang: ctx.locale
102
+ });
103
+ const versionsByEntry = new Map();
104
+ for (const v of versions) {
105
+ const arr = versionsByEntry.get(v.entryId) ?? [];
106
+ arr.push(v);
107
+ versionsByEntry.set(v.entryId, arr);
108
+ }
109
+ // Lazy import to avoid static circular dep with populateEntry → resolveRelationFields
110
+ const { _populate } = await import('./populateEntry.js');
111
+ await Promise.all(aliveDbEntries.map(async (dbEntry) => {
112
+ const vs = versionsByEntry.get(dbEntry.id) ?? [];
113
+ const picked = pickVersion(vs, ctx.status);
114
+ if (!picked) {
115
+ entriesMap[dbEntry.id] = null; // strict: missing version in locale → null
116
+ return;
117
+ }
118
+ let config;
119
+ try {
120
+ config = cms.getBySlug(dbEntry.slug);
121
+ }
122
+ catch {
123
+ entriesMap[dbEntry.id] = null;
124
+ return;
125
+ }
126
+ const nestedFields = getFieldsFromConfig(config);
127
+ const nestedCtx = {
128
+ locale: ctx.locale,
129
+ status: ctx.status,
130
+ depth: ctx.depth + 1,
131
+ maxDepth: ctx.maxDepth,
132
+ visited: new Set([...ctx.visited, dbEntry.id]),
133
+ populate: ctx.populate,
134
+ entryId: dbEntry.id
135
+ };
136
+ try {
137
+ const populated = await _populate(picked.data, nestedFields, nestedCtx);
138
+ const slugPath = getEntrySlugPath(dbEntry.slug);
139
+ const slugValue = getSlugFromEntryData(picked.data, slugPath, ctx.locale);
140
+ const _url = slugValue ? getEntryPath(dbEntry.slug, slugValue) : undefined;
141
+ entriesMap[dbEntry.id] = {
142
+ _id: dbEntry.id,
143
+ _slug: dbEntry.slug,
144
+ _type: dbEntry.type,
145
+ _publishedAt: picked.publishedAt,
146
+ _url,
147
+ ...(config.type === 'collection' && config.orderable
148
+ ? { _sortOrder: dbEntry.sortOrder }
149
+ : {}),
150
+ ...populated
151
+ };
152
+ }
153
+ catch (error) {
154
+ console.error(`[CMS] Failed to populate nested entry ${dbEntry.id} (${dbEntry.slug}):`, error);
155
+ }
156
+ }));
157
+ }
158
+ }
159
+ const resolveRefId = (id) => {
160
+ // Has key (even if value is null) → use mapped value (Entry | null)
161
+ if (Object.prototype.hasOwnProperty.call(entriesMap, id))
162
+ return entriesMap[id];
163
+ // Not fetched (cycle, max depth, top-level opt-out) → raw ID
164
+ return id;
165
+ };
69
166
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
- const resolveValues = (value, fields) => {
71
- // Start with a copy of all original data to preserve non-field properties
167
+ const resolveValues = (value, fields, topLevel) => {
72
168
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
169
  const result = { ...value };
74
170
  for (const field of fields) {
@@ -77,33 +173,43 @@ export async function resolveRelationFields(data, fields, language) {
77
173
  result[field.slug] = val;
78
174
  continue;
79
175
  }
176
+ // Per-field opt-out at top level → raw passthrough
177
+ if (topLevel && optedOutFields.has(field.slug)) {
178
+ result[field.slug] = val;
179
+ continue;
180
+ }
80
181
  switch (field.type) {
81
182
  case 'relation': {
82
183
  if (field.multiple && Array.isArray(val)) {
83
- result[field.slug] = val.map((id) => entriesMap[id] ?? null);
184
+ result[field.slug] = val.map((id) => typeof id === 'string' ? resolveRefId(id) : id);
185
+ }
186
+ else if (typeof val === 'string') {
187
+ result[field.slug] = resolveRefId(val);
84
188
  }
85
189
  else {
86
- result[field.slug] = entriesMap[val] ?? null;
190
+ result[field.slug] = val;
87
191
  }
88
192
  break;
89
193
  }
90
194
  case 'object':
91
- result[field.slug] = resolveValues(val, field.fields);
195
+ result[field.slug] = resolveValues(val, field.fields, false);
92
196
  break;
93
197
  case 'blocks':
94
198
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
95
199
  result[field.slug] = val.map((item) => {
96
200
  const objectDef = field.of.find((objDef) => objDef.slug === item._slug);
97
201
  if (objectDef) {
98
- return resolveValues(item, objectDef.fields);
202
+ return resolveValues(item, objectDef.fields, false);
99
203
  }
100
204
  return item;
101
205
  });
102
206
  break;
103
207
  case 'content': {
104
208
  const cf = field;
105
- // Content is now a single doc, not Record<lang, doc>
106
- if (val && typeof val === 'object' && val.type === 'doc' && cf.inlineBlocks?.length) {
209
+ if (val &&
210
+ typeof val === 'object' &&
211
+ val.type === 'doc' &&
212
+ cf.inlineBlocks?.length) {
107
213
  const cloned = cloneDoc(val);
108
214
  walkInlineBlockNodes(cloned, (node) => {
109
215
  const bd = node.attrs?.blockData;
@@ -112,7 +218,7 @@ export async function resolveRelationFields(data, fields, language) {
112
218
  const def = cf.inlineBlocks.find((b) => b.slug === node.attrs?.blockType);
113
219
  if (!def)
114
220
  return;
115
- node.attrs.blockData = resolveValues(bd, def.fields);
221
+ node.attrs.blockData = resolveValues(bd, def.fields, false);
116
222
  });
117
223
  result[field.slug] = cloned;
118
224
  }
@@ -124,5 +230,5 @@ export async function resolveRelationFields(data, fields, language) {
124
230
  }
125
231
  return result;
126
232
  };
127
- return resolveValues(data, fields);
233
+ return resolveValues(data, fields, true);
128
234
  }
@@ -1,3 +1,4 @@
1
1
  import type { EntryData, PopulatedEntryData } from '../../../types/entries.js';
2
2
  import type { Field } from '../../../types/fields.js';
3
- export declare function resolveRichtextLinks(data: EntryData, fields: Field[], language: string): Promise<PopulatedEntryData>;
3
+ import type { PopulateCtx } from '../entries/operations/resolveEntry.js';
4
+ export declare function resolveRichtextLinks(data: EntryData, fields: Field[], ctx: PopulateCtx): Promise<PopulatedEntryData>;
@@ -34,7 +34,8 @@ function resolveContentDoc(doc, slugMap) {
34
34
  });
35
35
  return cloned;
36
36
  }
37
- export async function resolveRichtextLinks(data, fields, language) {
37
+ export async function resolveRichtextLinks(data, fields, ctx) {
38
+ const language = ctx.locale;
38
39
  const entriesIds = [];
39
40
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
41
  const collectIds = (value, fields) => {
@@ -1,3 +1,4 @@
1
1
  import type { EntryData, PopulatedEntryData } from '../../../types/entries.js';
2
2
  import type { Field } from '../../../types/fields.js';
3
- export declare function resolveUrlFields(data: EntryData, fields: Field[], language?: string): Promise<PopulatedEntryData>;
3
+ import type { PopulateCtx } from '../entries/operations/resolveEntry.js';
4
+ export declare function resolveUrlFields(data: EntryData, fields: Field[], ctx: PopulateCtx): Promise<PopulatedEntryData>;
@@ -1,6 +1,6 @@
1
1
  import { urlFieldDataSchema, urlFieldDataWithRelationSchema } from '../../../schemas/field/url.js';
2
2
  import { walkInlineBlockNodes, cloneDoc } from '../../../admin/components/tiptap/structured-content-utils.js';
3
- import { getDbEntries, getDbEntryVersions } from '../entries/operations/get.js';
3
+ import { _getDbEntries, _getDbEntryVersions } from '../entries/operations/get.js';
4
4
  import { getEntrySlugPath, getSlugFromEntryData, getEntryPath } from './slugResolver.js';
5
5
  import { isExternalUrl, mergeRel } from '../../fields/urlUtils.js';
6
6
  const FLAT_KEY = '_flat';
@@ -34,7 +34,8 @@ function applyExternalAutoDetect(resolvedUrl, extras) {
34
34
  }
35
35
  }
36
36
  }
37
- export async function resolveUrlFields(data, fields, language) {
37
+ export async function resolveUrlFields(data, fields, ctx) {
38
+ const language = ctx.locale;
38
39
  const entriesIds = [];
39
40
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
41
  const collectIds = (value, fields) => {
@@ -102,12 +103,12 @@ export async function resolveUrlFields(data, fields, language) {
102
103
  collectIds(data, fields);
103
104
  const slugMap = {};
104
105
  if (entriesIds.length > 0) {
105
- // Use raw DB calls to avoid recursive populateEntryData → resolveUrlFields loop
106
- const dbEntries = await getDbEntries({ ids: entriesIds });
106
+ // Use raw DB calls to avoid recursive _populate → resolveUrlFields loop
107
+ const dbEntries = await _getDbEntries({ ids: entriesIds });
107
108
  const entryIds = dbEntries.map((e) => e.id);
108
109
  if (entryIds.length > 0) {
109
110
  // Get published versions for the target language
110
- const versions = await getDbEntryVersions({
111
+ const versions = await _getDbEntryVersions({
111
112
  entryIds,
112
113
  lang: language
113
114
  });
@@ -179,21 +179,21 @@ function generateAPI(config) {
179
179
  const filePath = join(cmsDir, 'api.ts');
180
180
  let code = `// This file is auto-generated. Do not edit directly.\n\n`;
181
181
  code += `
182
-
182
+
183
183
  import type { SingleEntryMap, SingleSlug, CollectionEntryMap, CollectionSlug, FormEntryMap, SiteLanguage } from './types';
184
- import { getEntry, getEntries, countEntries, createFormSubmission } from 'includio-cms/sveltekit/server';
184
+ import { resolveEntry, resolveEntries, countEntries, createFormSubmission, type PopulateConfig } from 'includio-cms/sveltekit/server';
185
185
 
186
186
  `;
187
187
  code += `
188
188
 
189
189
  interface GetEntryOptions {
190
190
  id?: string;
191
- status?: 'draft' | 'published' | 'scheduled' | 'archived';
192
- dataValues?: Record<string, unknown>;
191
+ status?: 'published' | 'draft' | 'scheduled';
192
+ populate?: PopulateConfig;
193
193
  `;
194
194
  if (config.languages && config.languages.length > 0) {
195
195
  code += `
196
- language?: SiteLanguage;
196
+ locale?: SiteLanguage;
197
197
  `;
198
198
  }
199
199
  code += `
@@ -201,8 +201,11 @@ function generateAPI(config) {
201
201
 
202
202
  interface GetEntriesOptions extends GetEntryOptions {
203
203
  ids?: string[];
204
- dataLike?: Record<string, unknown>;
205
- dataILikeOr?: Record<string, unknown>;
204
+ filter?: {
205
+ dataValues?: Record<string, unknown>;
206
+ dataLike?: Record<string, unknown>;
207
+ dataILikeOr?: Record<string, unknown>;
208
+ };
206
209
  orderBy?: { column: 'createdAt' | 'updatedAt' | 'sortOrder'; direction: 'asc' | 'desc' };
207
210
  dataOrderBy?: { field: string; direction: 'asc' | 'desc' };
208
211
  limit?: number;
@@ -213,25 +216,25 @@ function generateAPI(config) {
213
216
  code += `
214
217
 
215
218
  export async function getSingleEntry<K extends SingleSlug>(slug: K, options: GetEntryOptions = {}): Promise<SingleEntryMap[K] | null> {
216
- return (await getEntry({
217
- slug,
219
+ return (await resolveEntry({
220
+ collection: slug,
218
221
  ...options
219
222
  })) as unknown as SingleEntryMap[K] | null;
220
223
  }
221
224
 
222
225
  export async function getCollectionEntry<K extends CollectionSlug>(slug: K, options: GetEntryOptions = {}): Promise<CollectionEntryMap[K] | null> {
223
- return (await getEntry({
224
- slug,
226
+ return (await resolveEntry({
227
+ collection: slug,
225
228
  ...options
226
229
  })) as unknown as CollectionEntryMap[K] | null;
227
230
  }
228
231
 
229
232
  export async function getCollectionEntries<K extends CollectionSlug>(slug: K, options: GetEntriesOptions = {}): Promise<CollectionEntryMap[K][]> {
230
- return (await getEntries({ slug, ...options })) as unknown as CollectionEntryMap[K][];
233
+ return (await resolveEntries({ collection: slug, ...options })) as unknown as CollectionEntryMap[K][];
231
234
  }
232
235
 
233
- export async function countCollectionEntries<K extends CollectionSlug>(slug: K, options: Omit<GetEntriesOptions, 'limit' | 'offset' | 'orderBy'> = {}): Promise<number> {
234
- return countEntries({ slug, ...options });
236
+ export async function countCollectionEntries<K extends CollectionSlug>(slug: K, options: Omit<GetEntriesOptions, 'limit' | 'offset' | 'orderBy' | 'populate'> = {}): Promise<number> {
237
+ return countEntries({ collection: slug, ...options });
235
238
  }
236
239
 
237
240
  ${config.forms && config.forms.length > 0
@@ -1,6 +1,6 @@
1
1
  import { generateZodSchemaFromFields } from '../core/fields/fieldSchemaToTs.js';
2
2
  import { getFieldsFromConfig } from '../core/fields/layoutUtils.js';
3
- import { getRawEntries } from '../core/server/entries/operations/get.js';
3
+ import { _getRawEntries as getRawEntries } from '../core/server/entries/operations/get.js';
4
4
  export function createEntityAPI(cms, opts) {
5
5
  const db = cms.databaseAdapter;
6
6
  const userId = opts?.userId ?? 'system';
@@ -1,4 +1,5 @@
1
1
  import type { Field } from '../../types/fields.js';
2
+ import type { PopulateCtx } from '../../core/server/entries/operations/resolveEntry.js';
2
3
  export interface PopulatedShopField {
3
4
  /** Netto w PLN (number, ≤6dp). Od 0.15.2. */
4
5
  basePrice: number;
@@ -14,4 +15,4 @@ export interface PopulatedShopField {
14
15
  attributes: Record<string, string> | null;
15
16
  }>;
16
17
  }
17
- export declare function resolveShopFields(data: Record<string, unknown>, fields: Field[], entryId: string | undefined): Promise<Record<string, unknown>>;
18
+ export declare function resolveShopFields(data: Record<string, unknown>, fields: Field[], ctx: PopulateCtx): Promise<Record<string, unknown>>;
@@ -15,7 +15,8 @@ function toPopulated(row) {
15
15
  }))
16
16
  };
17
17
  }
18
- export async function resolveShopFields(data, fields, entryId) {
18
+ export async function resolveShopFields(data, fields, ctx) {
19
+ const entryId = ctx.entryId;
19
20
  if (!entryId)
20
21
  return data;
21
22
  // Skip if shop is not configured
@@ -1,6 +1,6 @@
1
1
  export { includioCMS } from './handle.js';
2
2
  export { cmsLayoutLoad } from './layout.js';
3
- export { getEntry, getEntries, countEntries } from '../../core/server/entries/operations/get.js';
3
+ export { resolveEntry, resolveEntries, countEntries, type ResolveEntryOptions, type ResolveEntriesOptions, type CountEntriesOptions, type PopulateConfig, type ResolveStatus } from '../../core/server/entries/operations/resolveEntry.js';
4
4
  export { createFormSubmission } from '../../core/server/forms/submissions/operations/create.js';
5
5
  export { parseFormDataForSubmission } from '../../core/server/forms/submissions/utils/parseMultipart.js';
6
6
  export { createConsentLog } from '../../core/server/consentLogs/operations/create.js';
@@ -1,6 +1,6 @@
1
1
  export { includioCMS } from './handle.js';
2
2
  export { cmsLayoutLoad } from './layout.js';
3
- export { getEntry, getEntries, countEntries } from '../../core/server/entries/operations/get.js';
3
+ export { resolveEntry, resolveEntries, countEntries } from '../../core/server/entries/operations/resolveEntry.js';
4
4
  export { createFormSubmission } from '../../core/server/forms/submissions/operations/create.js';
5
5
  export { parseFormDataForSubmission } from '../../core/server/forms/submissions/utils/parseMultipart.js';
6
6
  export { createConsentLog } from '../../core/server/consentLogs/operations/create.js';
@@ -2,6 +2,7 @@ import type { Component } from 'svelte';
2
2
  import type { z } from 'zod';
3
3
  import type { RawEntry } from './entries.js';
4
4
  import type { CustomField } from './fields.js';
5
+ import type { PopulateCtx } from '../core/server/entries/operations/resolveEntry.js';
5
6
  export interface CustomFieldDefinition {
6
7
  /** Unique field type slug, e.g. 'photo-grid' */
7
8
  fieldType: string;
@@ -13,8 +14,11 @@ export interface CustomFieldDefinition {
13
14
  zodSchema: (field: CustomField, languages: string[]) => z.ZodType;
14
15
  /** TypeScript type string for codegen */
15
16
  tsType?: string;
16
- /** Populate/resolve step for API output */
17
- populateResolver?: (value: unknown, field: CustomField) => Promise<unknown>;
17
+ /**
18
+ * Populate/resolve step for API output.
19
+ * @experimental ctx (3rd arg) added in 0.17.0 — carries locale/status/depth/visited cascade.
20
+ */
21
+ populateResolver?: (value: unknown, field: CustomField, ctx: PopulateCtx) => Promise<unknown>;
18
22
  }
19
23
  export interface PluginConfig {
20
24
  slug: string;
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;