includio-cms 0.16.0 → 0.19.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 +122 -0
- package/DOCS.md +1 -1
- package/README.md +62 -0
- 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/ai-claude/index.d.ts +9 -0
- package/dist/ai-claude/index.js +23 -7
- package/dist/ai-openai/index.d.ts +9 -0
- package/dist/ai-openai/index.js +28 -9
- 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/db-postgres/index.d.ts +4 -0
- package/dist/db-postgres/index.js +4 -0
- package/dist/email-nodemailer/index.d.ts +9 -0
- package/dist/email-nodemailer/index.js +28 -6
- package/dist/entity/index.js +1 -1
- package/dist/files-local/index.d.ts +4 -0
- package/dist/files-local/index.js +4 -0
- package/dist/paraglide/messages/_index.d.ts +3 -36
- package/dist/paraglide/messages/_index.js +3 -71
- package/dist/paraglide/messages/hello_world.d.ts +5 -0
- package/dist/paraglide/messages/hello_world.js +33 -0
- package/dist/paraglide/messages/login_hello.d.ts +16 -0
- package/dist/paraglide/messages/login_hello.js +34 -0
- package/dist/paraglide/messages/login_please_login.d.ts +16 -0
- package/dist/paraglide/messages/login_please_login.js +34 -0
- 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/adapters/ai.d.ts +8 -0
- package/dist/types/adapters/db.d.ts +9 -0
- package/dist/types/adapters/email.d.ts +6 -0
- package/dist/types/adapters/files.d.ts +5 -0
- package/dist/types/plugins.d.ts +6 -2
- package/dist/updates/0.18.0/index.d.ts +2 -0
- package/dist/updates/0.18.0/index.js +78 -0
- package/dist/updates/0.19.0/index.d.ts +2 -0
- package/dist/updates/0.19.0/index.js +40 -0
- package/dist/updates/index.js +3 -1
- package/package.json +14 -5
- package/dist/paraglide/messages/en.d.ts +0 -5
- package/dist/paraglide/messages/en.js +0 -14
- package/dist/paraglide/messages/pl.d.ts +0 -5
- package/dist/paraglide/messages/pl.js +0 -14
|
@@ -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(),
|
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
import type { EntryData, PopulatedEntryData } from '../../../types/entries.js';
|
|
2
2
|
import type { Field } from '../../../types/fields.js';
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
73
|
+
populated = resolveTypographyOrphans(populated, fields);
|
|
70
74
|
}
|
|
71
|
-
return
|
|
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
|
-
|
|
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
|
|
3
|
-
|
|
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
|
-
|
|
15
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
//
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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) =>
|
|
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] =
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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 {
|
|
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,
|
|
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
|
|
106
|
-
const dbEntries = await
|
|
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
|
|
111
|
+
const versions = await _getDbEntryVersions({
|
|
111
112
|
entryIds,
|
|
112
113
|
lang: language
|
|
113
114
|
});
|