includio-cms 0.37.2 → 0.37.4
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/API.md +3 -2
- package/CHANGELOG.md +19 -0
- package/DOCS.md +1 -1
- package/ROADMAP.md +21 -0
- package/dist/core/cms.d.ts +4 -1
- package/dist/core/cms.js +9 -2
- package/dist/core/fields/slugPath.d.ts +1 -1
- package/dist/core/fields/slugPath.js +3 -2
- package/dist/core/i18n/localeUrl.d.ts +20 -0
- package/dist/core/i18n/localeUrl.js +36 -0
- package/dist/core/server/entries/operations/get.js +1 -1
- package/dist/core/server/entries/operations/resolveEntry.d.ts +4 -2
- package/dist/core/server/entries/operations/resolveEntry.js +25 -43
- package/dist/core/server/entries/operations/versionResolve.d.ts +39 -0
- package/dist/core/server/entries/operations/versionResolve.js +52 -0
- package/dist/core/server/fields/populateSelect.d.ts +22 -0
- package/dist/core/server/fields/populateSelect.js +28 -0
- package/dist/core/server/fields/resolveRelationFields.d.ts +8 -8
- package/dist/core/server/fields/resolveRelationFields.js +135 -113
- package/dist/core/server/fields/resolveRichtextLinks.js +1 -1
- package/dist/core/server/fields/resolveUrlFields.js +1 -1
- package/dist/core/server/fields/slugResolver.d.ts +2 -2
- package/dist/core/server/fields/slugResolver.js +12 -4
- package/dist/core/server/generator/generator.js +60 -1
- package/dist/db-postgres/index.js +48 -0
- package/dist/db-postgres/schema/entry.js +2 -2
- package/dist/db-postgres/schema/entryVersion.js +5 -2
- package/dist/db-postgres/schema/mediaFileTag.js +4 -2
- package/dist/sveltekit/server/handle.js +5 -5
- package/dist/types/adapters/db.d.ts +13 -1
- package/dist/types/cms.d.ts +3 -2
- package/dist/types/cms.schema.d.ts +22 -0
- package/dist/types/cms.schema.js +15 -3
- package/dist/types/collections.d.ts +3 -0
- package/dist/types/entries.d.ts +2 -0
- package/dist/types/fields.d.ts +12 -0
- package/dist/types/languages.d.ts +13 -0
- package/dist/types/languages.js +2 -1
- package/dist/updates/0.37.4/index.d.ts +2 -0
- package/dist/updates/0.37.4/index.js +17 -0
- package/dist/updates/index.js +3 -1
- package/package.json +1 -1
|
@@ -2,53 +2,73 @@ import { walkInlineBlockNodes, cloneDoc } from '../../../admin/components/tiptap
|
|
|
2
2
|
import { getCMS } from '../../cms.js';
|
|
3
3
|
import { getFieldsFromConfig } from '../../fields/layoutUtils.js';
|
|
4
4
|
import { getEntrySlugPath, getSlugFromEntryData, getEntryPath } from './slugResolver.js';
|
|
5
|
+
import { resolveEffectivePopulate, narrowFields, narrowData, populateConfigKey } from './populateSelect.js';
|
|
6
|
+
import { pickVersion as pickVersionBase, pickAndHydrateVersions } from '../entries/operations/versionResolve.js';
|
|
5
7
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* drafted (only published versions) must still resolve, otherwise draft previews
|
|
10
|
-
* silently drop every relation to `null` — see preview.remote.ts, which populates
|
|
11
|
-
* form data with `status: 'draft'`.
|
|
8
|
+
* Wersja *populowanej relacji*: draft zawsze z fallbackiem na published — relacja,
|
|
9
|
+
* która nigdy nie była szkicowana (tylko published), musi się rozwiązać, inaczej
|
|
10
|
+
* podgląd szkicu cicho zeruje każdą relację (patrz preview.remote.ts). Eksport dla testów.
|
|
12
11
|
*/
|
|
13
12
|
export function pickVersion(versions, status) {
|
|
14
|
-
|
|
15
|
-
const now = new Date();
|
|
16
|
-
const published = () => sorted.find((v) => v.publishedAt != null && v.publishedAt <= now) ?? null;
|
|
17
|
-
switch (status) {
|
|
18
|
-
case 'published':
|
|
19
|
-
return published();
|
|
20
|
-
case 'draft':
|
|
21
|
-
return sorted.find((v) => v.publishedAt == null) ?? published();
|
|
22
|
-
case 'scheduled':
|
|
23
|
-
return sorted.find((v) => v.publishedAt != null && v.publishedAt > now) ?? null;
|
|
24
|
-
}
|
|
13
|
+
return pickVersionBase(versions, status, { draftFallback: true });
|
|
25
14
|
}
|
|
26
15
|
export async function resolveRelationFields(data, fields, ctx) {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
16
|
+
const cms = getCMS();
|
|
17
|
+
// key `${id}::${configKey}` — ten sam target pod różnymi configami populuje się osobno.
|
|
18
|
+
const configByKey = new Map();
|
|
19
|
+
/** Efektywny config dla pola relacji. `null` = denylist (surowe ID). */
|
|
20
|
+
const effForField = (field, topLevel) => {
|
|
21
|
+
const callOv = topLevel ? ctx.populate.fields?.[field.slug] : undefined;
|
|
22
|
+
if (callOv === false)
|
|
23
|
+
return null; // top-level denylist → raw passthrough
|
|
24
|
+
let collectionDefault;
|
|
25
|
+
try {
|
|
26
|
+
const target = cms.getBySlug(field.collection);
|
|
27
|
+
collectionDefault = target.defaultPopulate;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
collectionDefault = undefined;
|
|
31
|
+
}
|
|
32
|
+
const cfg = resolveEffectivePopulate({
|
|
33
|
+
callOverride: callOv,
|
|
34
|
+
fieldPopulate: field.populate,
|
|
35
|
+
collectionDefault
|
|
36
|
+
});
|
|
37
|
+
const key = populateConfigKey(cfg);
|
|
38
|
+
configByKey.set(key, cfg);
|
|
39
|
+
return { cfg, key };
|
|
40
|
+
};
|
|
41
|
+
// Zbierz pary (id, configKey) do populacji.
|
|
42
|
+
const wanted = [];
|
|
43
|
+
const seenPair = new Set();
|
|
44
|
+
const addId = (id, key) => {
|
|
45
|
+
const pk = `${id}::${key}`;
|
|
46
|
+
if (seenPair.has(pk))
|
|
47
|
+
return;
|
|
48
|
+
seenPair.add(pk);
|
|
49
|
+
wanted.push({ id, key });
|
|
50
|
+
};
|
|
30
51
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
52
|
const collectIds = (value, fields, topLevel) => {
|
|
32
53
|
for (const field of fields) {
|
|
33
54
|
const val = value?.[field.slug];
|
|
34
55
|
if (val == null)
|
|
35
56
|
continue;
|
|
36
|
-
if (topLevel && ctx.populate.fields?.[field.slug] === false) {
|
|
37
|
-
optedOutFields.add(field.slug);
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
57
|
switch (field.type) {
|
|
41
58
|
case 'relation': {
|
|
59
|
+
const eff = effForField(field, topLevel);
|
|
60
|
+
if (!eff)
|
|
61
|
+
break; // denylist → nie fetchuj
|
|
42
62
|
// Skip empty strings — an unset optional relation stores '' and must
|
|
43
63
|
// never reach the id query (Postgres rejects '' as uuid).
|
|
44
64
|
if (field.multiple && Array.isArray(val)) {
|
|
45
65
|
for (const id of val) {
|
|
46
66
|
if (typeof id === 'string' && id !== '')
|
|
47
|
-
|
|
67
|
+
addId(id, eff.key);
|
|
48
68
|
}
|
|
49
69
|
}
|
|
50
70
|
else if (typeof val === 'string' && val !== '') {
|
|
51
|
-
|
|
71
|
+
addId(val, eff.key);
|
|
52
72
|
}
|
|
53
73
|
break;
|
|
54
74
|
}
|
|
@@ -86,92 +106,94 @@ export async function resolveRelationFields(data, fields, ctx) {
|
|
|
86
106
|
}
|
|
87
107
|
};
|
|
88
108
|
collectIds(data, fields, true);
|
|
89
|
-
if (
|
|
109
|
+
if (wanted.length === 0)
|
|
90
110
|
return data;
|
|
91
|
-
//
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
111
|
+
// Fetchowalność per (id, config): cap głębokości per-pole + brak cyklu.
|
|
112
|
+
const fetchable = wanted.filter(({ id, key }) => {
|
|
113
|
+
if (ctx.visited.has(id))
|
|
114
|
+
return false;
|
|
115
|
+
const cfg = configByKey.get(key) ?? null;
|
|
116
|
+
const fieldMax = cfg?.depth != null ? ctx.depth + 1 + cfg.depth : ctx.maxDepth;
|
|
117
|
+
return ctx.depth < fieldMax;
|
|
118
|
+
});
|
|
119
|
+
// entriesMap: `${id}::${key}` → Entry (populated) | null (fetched but missing).
|
|
120
|
+
// Brak klucza = nie fetchowano (cykl / max depth / denylist) → fallback do raw id.
|
|
99
121
|
const entriesMap = {};
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
122
|
+
const fetchIds = [...new Set(fetchable.map((p) => p.id))];
|
|
123
|
+
if (fetchIds.length > 0) {
|
|
124
|
+
const dbEntries = await cms.databaseAdapter.getEntries({ ids: fetchIds });
|
|
125
|
+
const aliveById = new Map(dbEntries.filter((e) => e.archivedAt == null).map((e) => [e.id, e]));
|
|
126
|
+
const versionMap = await pickAndHydrateVersions({
|
|
127
|
+
adapter: cms.databaseAdapter,
|
|
128
|
+
entryIds: [...aliveById.keys()],
|
|
129
|
+
lang: ctx.locale,
|
|
130
|
+
status: ctx.status,
|
|
131
|
+
draftFallback: true
|
|
132
|
+
});
|
|
133
|
+
// Lazy import to avoid static circular dep with populateEntry → resolveRelationFields
|
|
134
|
+
const { _populate } = await import('./populateEntry.js');
|
|
135
|
+
await Promise.all(fetchable.map(async ({ id, key }) => {
|
|
136
|
+
const mk = `${id}::${key}`;
|
|
137
|
+
const dbEntry = aliveById.get(id);
|
|
138
|
+
if (!dbEntry) {
|
|
139
|
+
entriesMap[mk] = null; // strict: missing / archived → null
|
|
140
|
+
return;
|
|
119
141
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
const picked = versionMap.get(id);
|
|
143
|
+
if (!picked) {
|
|
144
|
+
entriesMap[mk] = null; // strict: missing version in locale → null
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
let config;
|
|
148
|
+
try {
|
|
149
|
+
config = cms.getBySlug(dbEntry.slug);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
entriesMap[mk] = null;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const cfg = configByKey.get(key) ?? null;
|
|
156
|
+
const allFields = getFieldsFromConfig(config);
|
|
157
|
+
const nestedFields = cfg?.select ? narrowFields(allFields, cfg.select) : allFields;
|
|
158
|
+
const nestedData = cfg?.select ? narrowData(picked.data, cfg.select) : picked.data;
|
|
159
|
+
const nestedMaxDepth = cfg?.depth != null ? ctx.depth + 1 + cfg.depth : ctx.maxDepth;
|
|
160
|
+
const nestedCtx = {
|
|
161
|
+
locale: ctx.locale,
|
|
162
|
+
status: ctx.status,
|
|
163
|
+
depth: ctx.depth + 1,
|
|
164
|
+
maxDepth: nestedMaxDepth,
|
|
165
|
+
visited: new Set([...ctx.visited, dbEntry.id]),
|
|
166
|
+
populate: ctx.populate,
|
|
167
|
+
entryId: dbEntry.id
|
|
168
|
+
};
|
|
169
|
+
try {
|
|
170
|
+
const populated = await _populate(nestedData, nestedFields, nestedCtx);
|
|
171
|
+
const slugPath = getEntrySlugPath(dbEntry.slug);
|
|
172
|
+
const slugValue = getSlugFromEntryData(picked.data, slugPath, ctx.locale);
|
|
173
|
+
const _url = slugValue ? getEntryPath(dbEntry.slug, slugValue, ctx.locale) : undefined;
|
|
174
|
+
entriesMap[mk] = {
|
|
175
|
+
_id: dbEntry.id,
|
|
176
|
+
_slug: dbEntry.slug,
|
|
177
|
+
_type: dbEntry.type,
|
|
178
|
+
_publishedAt: picked.publishedAt,
|
|
179
|
+
_url,
|
|
180
|
+
...(config.type === 'collection' && config.orderable
|
|
181
|
+
? { _sortOrder: dbEntry.sortOrder }
|
|
182
|
+
: {}),
|
|
183
|
+
...populated
|
|
146
184
|
};
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
entriesMap[dbEntry.id] = {
|
|
153
|
-
_id: dbEntry.id,
|
|
154
|
-
_slug: dbEntry.slug,
|
|
155
|
-
_type: dbEntry.type,
|
|
156
|
-
_publishedAt: picked.publishedAt,
|
|
157
|
-
_url,
|
|
158
|
-
...(config.type === 'collection' && config.orderable
|
|
159
|
-
? { _sortOrder: dbEntry.sortOrder }
|
|
160
|
-
: {}),
|
|
161
|
-
...populated
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
catch (error) {
|
|
165
|
-
console.error(`[CMS] Failed to populate nested entry ${dbEntry.id} (${dbEntry.slug}):`, error);
|
|
166
|
-
}
|
|
167
|
-
}));
|
|
168
|
-
}
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
console.error(`[CMS] Failed to populate nested entry ${dbEntry.id} (${dbEntry.slug}):`, error);
|
|
188
|
+
}
|
|
189
|
+
}));
|
|
169
190
|
}
|
|
170
|
-
const resolveRefId = (id) => {
|
|
191
|
+
const resolveRefId = (id, key) => {
|
|
192
|
+
const mk = `${id}::${key}`;
|
|
171
193
|
// Has key (even if value is null) → use mapped value (Entry | null)
|
|
172
|
-
if (Object.prototype.hasOwnProperty.call(entriesMap,
|
|
173
|
-
return entriesMap[
|
|
174
|
-
// Not fetched (cycle, max depth,
|
|
194
|
+
if (Object.prototype.hasOwnProperty.call(entriesMap, mk))
|
|
195
|
+
return entriesMap[mk];
|
|
196
|
+
// Not fetched (cycle, max depth, denylist) → raw ID
|
|
175
197
|
return id;
|
|
176
198
|
};
|
|
177
199
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -184,18 +206,18 @@ export async function resolveRelationFields(data, fields, ctx) {
|
|
|
184
206
|
result[field.slug] = val;
|
|
185
207
|
continue;
|
|
186
208
|
}
|
|
187
|
-
// Per-field opt-out at top level → raw passthrough
|
|
188
|
-
if (topLevel && optedOutFields.has(field.slug)) {
|
|
189
|
-
result[field.slug] = val;
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
209
|
switch (field.type) {
|
|
193
210
|
case 'relation': {
|
|
211
|
+
const eff = effForField(field, topLevel);
|
|
212
|
+
if (!eff) {
|
|
213
|
+
result[field.slug] = val; // denylist → raw passthrough
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
194
216
|
if (field.multiple && Array.isArray(val)) {
|
|
195
|
-
result[field.slug] = val.map((id) => typeof id === 'string' ? resolveRefId(id) : id);
|
|
217
|
+
result[field.slug] = val.map((id) => typeof id === 'string' ? resolveRefId(id, eff.key) : id);
|
|
196
218
|
}
|
|
197
219
|
else if (typeof val === 'string') {
|
|
198
|
-
result[field.slug] = resolveRefId(val);
|
|
220
|
+
result[field.slug] = resolveRefId(val, eff.key);
|
|
199
221
|
}
|
|
200
222
|
else {
|
|
201
223
|
result[field.slug] = val;
|
|
@@ -109,7 +109,7 @@ export async function resolveRichtextLinks(data, fields, ctx) {
|
|
|
109
109
|
const slugPath = configSlug ? getEntrySlugPath(configSlug) : 'seo.slug';
|
|
110
110
|
const slug = getSlugFromEntryData(rawData, slugPath, language);
|
|
111
111
|
if (slug) {
|
|
112
|
-
slugMap[entryId] = configSlug ? getEntryPath(configSlug, slug) : slug;
|
|
112
|
+
slugMap[entryId] = configSlug ? getEntryPath(configSlug, slug, language) : slug;
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -130,7 +130,7 @@ export async function resolveUrlFields(data, fields, ctx) {
|
|
|
130
130
|
const slugPath = getEntrySlugPath(entry.slug);
|
|
131
131
|
const slug = getSlugFromEntryData(version.data, slugPath, language);
|
|
132
132
|
if (slug)
|
|
133
|
-
slugMap[entry.id] = getEntryPath(entry.slug, slug);
|
|
133
|
+
slugMap[entry.id] = getEntryPath(entry.slug, slug, language);
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { getSlugFromEntryData } from '../../fields/slugPath.js';
|
|
2
2
|
/** Get dot-path to slug field for a collection. Falls back to 'seo.slug'. */
|
|
3
3
|
export declare function getEntrySlugPath(configSlug: string): string;
|
|
4
|
-
/** Build full entry path using collection's pathTemplate. */
|
|
5
|
-
export declare function getEntryPath(configSlug: string, slug: string): string;
|
|
4
|
+
/** Build full entry path using collection's pathTemplate, prefixed per locale. */
|
|
5
|
+
export declare function getEntryPath(configSlug: string, slug: string, locale?: string): string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getCMS } from '../../cms.js';
|
|
2
2
|
import { getSlugFromEntryData, resolveSlugPath, applyPathTemplate } from '../../fields/slugPath.js';
|
|
3
|
+
import { pathPrefixFor } from '../../i18n/localeUrl.js';
|
|
3
4
|
export { getSlugFromEntryData } from '../../fields/slugPath.js';
|
|
4
5
|
/** Get dot-path to slug field for a collection. Falls back to 'seo.slug'. */
|
|
5
6
|
export function getEntrySlugPath(configSlug) {
|
|
@@ -10,12 +11,19 @@ export function getEntrySlugPath(configSlug) {
|
|
|
10
11
|
return resolveSlugPath(undefined);
|
|
11
12
|
}
|
|
12
13
|
}
|
|
13
|
-
/** Build full entry path using collection's pathTemplate. */
|
|
14
|
-
export function getEntryPath(configSlug, slug) {
|
|
14
|
+
/** Build full entry path using collection's pathTemplate, prefixed per locale. */
|
|
15
|
+
export function getEntryPath(configSlug, slug, locale) {
|
|
16
|
+
let prefix = '';
|
|
15
17
|
try {
|
|
16
|
-
|
|
18
|
+
prefix = pathPrefixFor(getCMS().localeUrl, locale);
|
|
17
19
|
}
|
|
18
20
|
catch {
|
|
19
|
-
|
|
21
|
+
prefix = '';
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
return applyPathTemplate(getCMS().getBySlug(configSlug).pathTemplate, slug, prefix);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return applyPathTemplate(undefined, slug, prefix);
|
|
20
28
|
}
|
|
21
29
|
}
|
|
@@ -5,6 +5,8 @@ import { generateTsTypeFromFormFields } from './formFields.js';
|
|
|
5
5
|
import { generateZodSchemaStringFromFormFieldsAsString } from './formFieldSchemaToString.js';
|
|
6
6
|
import { toPascalCase, quoteKey } from './utils.js';
|
|
7
7
|
import { getFieldsFromConfig } from '../../fields/layoutUtils.js';
|
|
8
|
+
import { buildLocaleUrl } from '../../i18n/localeUrl.js';
|
|
9
|
+
import { languageCode } from '../../../types/languages.js';
|
|
8
10
|
function createCmsRuntimeDir() {
|
|
9
11
|
const cmsDir = join(process.cwd(), 'src/lib/cms/runtime');
|
|
10
12
|
mkdirSync(cmsDir, { recursive: true });
|
|
@@ -171,7 +173,7 @@ function generateTypes(config) {
|
|
|
171
173
|
}
|
|
172
174
|
}
|
|
173
175
|
}
|
|
174
|
-
code += `\n export type SiteLanguage = ${config.languages.map((lang) => JSON.stringify(lang)).join(' | ')};\n\n`;
|
|
176
|
+
code += `\n export type SiteLanguage = ${config.languages.map((lang) => JSON.stringify(languageCode(lang))).join(' | ')};\n\n`;
|
|
175
177
|
writeIfChanged(filePath, code);
|
|
176
178
|
}
|
|
177
179
|
function generateAPI(config) {
|
|
@@ -307,6 +309,62 @@ function generateRemote(config) {
|
|
|
307
309
|
});
|
|
308
310
|
writeIfChanged(filePath, code);
|
|
309
311
|
}
|
|
312
|
+
/**
|
|
313
|
+
* Emituje konfigurację app-owego Paraglide z `cms.config.languages` — jedno źródło prawdy i18n.
|
|
314
|
+
* `project.inlang/settings.json` (baseLocale=default, locales) + `src/lib/cms/runtime/i18n.ts`
|
|
315
|
+
* (`paraglideUrlPatterns` do vite.config + `reroute` do hooks). Konsument importuje te artefakty
|
|
316
|
+
* zamiast utrzymywać osobny, ręcznie synchronizowany config.
|
|
317
|
+
*/
|
|
318
|
+
function generateI18n(config) {
|
|
319
|
+
const lu = buildLocaleUrl(config.languages);
|
|
320
|
+
const codes = config.languages.map((l) => typeof l === 'string' ? l : l.code);
|
|
321
|
+
// 1) project.inlang/settings.json
|
|
322
|
+
const settings = {
|
|
323
|
+
$schema: 'https://inlang.com/schema/project-settings',
|
|
324
|
+
modules: [
|
|
325
|
+
'https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js',
|
|
326
|
+
'https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js'
|
|
327
|
+
],
|
|
328
|
+
'plugin.inlang.messageFormat': { pathPattern: './messages/{locale}.json' },
|
|
329
|
+
baseLocale: lu.defaultLocale,
|
|
330
|
+
locales: codes
|
|
331
|
+
};
|
|
332
|
+
const inlangDir = join(process.cwd(), 'project.inlang');
|
|
333
|
+
mkdirSync(inlangDir, { recursive: true });
|
|
334
|
+
writeIfChanged(join(inlangDir, 'settings.json'), JSON.stringify(settings, null, '\t') + '\n');
|
|
335
|
+
createCmsRuntimeDir();
|
|
336
|
+
// 2) src/lib/cms/runtime/i18n.ts — CZYSTE dane urlPatterns (bez importów paraglide),
|
|
337
|
+
// żeby dało się importować w vite.config.ts (paraglide runtime jeszcze nie istnieje przy config-load).
|
|
338
|
+
// KOLEJNOŚĆ: prefiksowane locale NAJPIERW, domyślny (catch-all `/:path`) OSTATNI — inaczej
|
|
339
|
+
// catch-all złapałby `/en/...` przed wzorcem `/en`.
|
|
340
|
+
const ordered = [...codes].sort((a, b) => {
|
|
341
|
+
const aDef = a === lu.defaultLocale ? 1 : 0;
|
|
342
|
+
const bDef = b === lu.defaultLocale ? 1 : 0;
|
|
343
|
+
return aDef - bDef;
|
|
344
|
+
});
|
|
345
|
+
const localized = ordered
|
|
346
|
+
.map((c) => {
|
|
347
|
+
const e = lu.byCode[c];
|
|
348
|
+
const p = e && e.strategy === 'pathPrefix' ? e.pathPrefix : '';
|
|
349
|
+
return `\t\t\t['${c}', '${p}/:path(.*)?']`;
|
|
350
|
+
})
|
|
351
|
+
.join(',\n');
|
|
352
|
+
// Buduj specyfikator `$lib/...` przez konkatenację — inaczej svelte-package (build rdzenia)
|
|
353
|
+
// potraktuje literał jak import w tym pliku i przepisze go na relatywną ścieżkę RDZENIA.
|
|
354
|
+
const paraglideRuntime = '$' + 'lib/paraglide/runtime';
|
|
355
|
+
const i18n = `// AUTOGENEROWANE przez includio-cms (generateRuntime). Nie edytuj ręcznie.\n` +
|
|
356
|
+
`// \`import type\` jest wymazywany przy buildzie → można importować w vite.config.ts bez cyklu runtime.\n` +
|
|
357
|
+
`import type { Locale } from '${paraglideRuntime}';\n\n` +
|
|
358
|
+
`export const paraglideUrlPatterns: Array<{ pattern: string; localized: Array<[Locale, string]> }> = [\n` +
|
|
359
|
+
`\t{\n\t\tpattern: '/:path(.*)?',\n\t\tlocalized: [\n${localized}\n\t\t]\n\t}\n];\n`;
|
|
360
|
+
writeIfChanged(join(process.cwd(), 'src/lib/cms/runtime/i18n.ts'), i18n);
|
|
361
|
+
// 3) src/lib/cms/runtime/reroute.ts — reroute (importuje paraglide runtime), do wpięcia w hooks.ts
|
|
362
|
+
const reroute = `// AUTOGENEROWANE przez includio-cms (generateRuntime). Nie edytuj ręcznie.\n` +
|
|
363
|
+
`import { deLocalizeUrl } from '${paraglideRuntime}';\n` +
|
|
364
|
+
`import type { Reroute } from '@sveltejs/kit';\n\n` +
|
|
365
|
+
`export const reroute: Reroute = (request) => deLocalizeUrl(request.url).pathname;\n`;
|
|
366
|
+
writeIfChanged(join(process.cwd(), 'src/lib/cms/runtime/reroute.ts'), reroute);
|
|
367
|
+
}
|
|
310
368
|
export function generateRuntime(config) {
|
|
311
369
|
// Build custom fields map from plugins for type generation
|
|
312
370
|
const customFields = new Map();
|
|
@@ -325,4 +383,5 @@ export function generateRuntime(config) {
|
|
|
325
383
|
generateSchemas(config);
|
|
326
384
|
generateDrizzleSchema(config);
|
|
327
385
|
generateRemote(config);
|
|
386
|
+
generateI18n(config);
|
|
328
387
|
}
|
|
@@ -494,6 +494,54 @@ export function pg(config) {
|
|
|
494
494
|
}
|
|
495
495
|
return await query;
|
|
496
496
|
},
|
|
497
|
+
getEntryVersionsMeta: async (options) => {
|
|
498
|
+
const cols = {
|
|
499
|
+
id: schema.entryVersionsTable.id,
|
|
500
|
+
entryId: schema.entryVersionsTable.entryId,
|
|
501
|
+
lang: schema.entryVersionsTable.lang,
|
|
502
|
+
versionNumber: schema.entryVersionsTable.versionNumber,
|
|
503
|
+
createdAt: schema.entryVersionsTable.createdAt,
|
|
504
|
+
createdBy: schema.entryVersionsTable.createdBy,
|
|
505
|
+
publishedAt: schema.entryVersionsTable.publishedAt,
|
|
506
|
+
publishedBy: schema.entryVersionsTable.publishedBy
|
|
507
|
+
};
|
|
508
|
+
const query = db.select(cols).from(schema.entryVersionsTable);
|
|
509
|
+
if (options) {
|
|
510
|
+
const baseConditions = [
|
|
511
|
+
options.entryIds
|
|
512
|
+
? inArray(schema.entryVersionsTable.entryId, options.entryIds)
|
|
513
|
+
: undefined,
|
|
514
|
+
options.ids ? inArray(schema.entryVersionsTable.id, options.ids) : undefined,
|
|
515
|
+
options.lang ? eq(schema.entryVersionsTable.lang, options.lang) : undefined
|
|
516
|
+
].filter(Boolean);
|
|
517
|
+
const jsonConditions = options.dataValues ? buildJsonConditions(options.dataValues) : [];
|
|
518
|
+
const jsonLikeConditions = options.dataLike
|
|
519
|
+
? buildJsonLikeConditions(options.dataLike)
|
|
520
|
+
: [];
|
|
521
|
+
const jsonILikeOrCondition = options.dataILikeOr
|
|
522
|
+
? buildJsonILikeOrConditions(options.dataILikeOr)
|
|
523
|
+
: undefined;
|
|
524
|
+
const allConditions = [
|
|
525
|
+
...baseConditions,
|
|
526
|
+
...jsonConditions,
|
|
527
|
+
...jsonLikeConditions,
|
|
528
|
+
...(jsonILikeOrCondition ? [jsonILikeOrCondition] : [])
|
|
529
|
+
];
|
|
530
|
+
if (allConditions.length > 0) {
|
|
531
|
+
query.where(and(...allConditions));
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return await query;
|
|
535
|
+
},
|
|
536
|
+
getEntryVersionData: async ({ picks, lang }) => {
|
|
537
|
+
if (picks.length === 0)
|
|
538
|
+
return [];
|
|
539
|
+
const pairConds = picks.map((p) => and(eq(schema.entryVersionsTable.entryId, p.entryId), eq(schema.entryVersionsTable.versionNumber, p.versionNumber)));
|
|
540
|
+
return await db
|
|
541
|
+
.select()
|
|
542
|
+
.from(schema.entryVersionsTable)
|
|
543
|
+
.where(and(eq(schema.entryVersionsTable.lang, lang), or(...pairConds)));
|
|
544
|
+
},
|
|
497
545
|
updateEntryVersion: async ({ id, data }) => {
|
|
498
546
|
return db.transaction(async (tx) => {
|
|
499
547
|
const [result] = await tx
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
|
|
1
|
+
import { index, integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
|
|
2
2
|
export const entriesTable = pgTable('entry', {
|
|
3
3
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
4
4
|
slug: text('slug').notNull(),
|
|
@@ -9,4 +9,4 @@ export const entriesTable = pgTable('entry', {
|
|
|
9
9
|
archivedAt: timestamp('archived_at'), // soft delete / archiwum
|
|
10
10
|
// Manual ordering
|
|
11
11
|
sortOrder: integer('sort_order')
|
|
12
|
-
});
|
|
12
|
+
}, (t) => [index('entry_slug_idx').on(t.slug), index('entry_type_idx').on(t.type)]);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { integer, jsonb, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
|
|
1
|
+
import { index, integer, jsonb, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
|
|
2
2
|
import { entriesTable } from './entry.js';
|
|
3
3
|
import {} from '../../types/entries.js';
|
|
4
4
|
export const entryVersionsTable = pgTable('entry_version', {
|
|
@@ -13,4 +13,7 @@ export const entryVersionsTable = pgTable('entry_version', {
|
|
|
13
13
|
createdBy: text('created_by'),
|
|
14
14
|
publishedAt: timestamp('published_at'),
|
|
15
15
|
publishedBy: text('published_by')
|
|
16
|
-
})
|
|
16
|
+
}, (t) => [
|
|
17
|
+
index('entry_version_entry_lang_ver_idx').on(t.entryId, t.lang, t.versionNumber),
|
|
18
|
+
index('entry_version_entry_published_idx').on(t.entryId, t.publishedAt)
|
|
19
|
+
]);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core';
|
|
1
|
+
import { index, pgTable, primaryKey, uuid } from 'drizzle-orm/pg-core';
|
|
2
2
|
import { mediaFilesTable } from './mediaFile.js';
|
|
3
3
|
import { mediaTagsTable } from './mediaTag.js';
|
|
4
4
|
export const mediaFileTagsTable = pgTable('media_file_tag', {
|
|
@@ -8,4 +8,6 @@ export const mediaFileTagsTable = pgTable('media_file_tag', {
|
|
|
8
8
|
tagId: uuid('tag_id')
|
|
9
9
|
.notNull()
|
|
10
10
|
.references(() => mediaTagsTable.id, { onDelete: 'cascade' })
|
|
11
|
-
},
|
|
11
|
+
},
|
|
12
|
+
// PK (file_id, tag_id) już pokrywa lookup po file_id; brakuje leading tag_id.
|
|
13
|
+
(t) => [primaryKey({ columns: [t.fileId, t.tagId] }), index('media_file_tag_tag_idx').on(t.tagId)]);
|
|
@@ -7,7 +7,8 @@ import { building } from '$app/environment';
|
|
|
7
7
|
import { csrfGuard } from '../../server/security/csrf.js';
|
|
8
8
|
import { rateLimitGuard } from '../../server/security/rate-limit.js';
|
|
9
9
|
import { buildCspHeader } from '../../server/security/csp.js';
|
|
10
|
-
import { baseLocale
|
|
10
|
+
import { baseLocale } from '../../paraglide/runtime.js';
|
|
11
|
+
import { localeFromPath } from '../../core/i18n/localeUrl.js';
|
|
11
12
|
const adminGuard = async ({ event, resolve }) => {
|
|
12
13
|
const { user, session } = event.locals;
|
|
13
14
|
// Secure the admin routes
|
|
@@ -62,12 +63,11 @@ const handleAuth = async ({ event, resolve }) => {
|
|
|
62
63
|
return svelteKitHandler({ event, resolve, auth, building });
|
|
63
64
|
};
|
|
64
65
|
const paraglideLang = async ({ event, resolve }) => {
|
|
66
|
+
// Locale storefrontu z URL (prefiks z cms.config.languages), config-driven, bez cookie.
|
|
67
|
+
// Trasy bez prefiksu (np. /admin) → defaultLocale.
|
|
65
68
|
let locale = baseLocale;
|
|
66
69
|
try {
|
|
67
|
-
|
|
68
|
-
if (detected && isLocale(detected)) {
|
|
69
|
-
locale = detected;
|
|
70
|
-
}
|
|
70
|
+
locale = localeFromPath(getCMS().localeUrl, event.url.pathname);
|
|
71
71
|
}
|
|
72
72
|
catch {
|
|
73
73
|
// fall back to baseLocale
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ConsentLogData, ConsentLogRecord, GetConsentLogsFilters } from '../consent.js';
|
|
2
|
-
import type { DbEntry, DbEntryInsert, DbEntryVersion, DbEntryVersionInsert, GetDbEntriesOptions, GetDbEntryVersionsOptions, GetPaginatedEntriesOptions, PaginatedEntryRow, PaginationOptions } from '../entries.js';
|
|
2
|
+
import type { DbEntry, DbEntryInsert, DbEntryVersion, DbEntryVersionInsert, DbEntryVersionMeta, GetDbEntriesOptions, GetDbEntryVersionsOptions, GetPaginatedEntriesOptions, PaginatedEntryRow, PaginationOptions } from '../entries.js';
|
|
3
3
|
import type { ImageFieldStyle } from '../fields.js';
|
|
4
4
|
import type { FormSubmission } from '../forms.js';
|
|
5
5
|
import type { ImageStyle, MediaFile, MediaTag, UploadedMediaFile, VideoStyle, VideoStyleStatus } from '../media.js';
|
|
@@ -23,6 +23,10 @@ export interface DatabaseAdapter {
|
|
|
23
23
|
createEntryVersion: CreateEntryVersion;
|
|
24
24
|
updateEntryVersion: UpdateEntryVersion;
|
|
25
25
|
getEntryVersions: GetEntryVersions;
|
|
26
|
+
/** Metadane wersji bez blobu `data` — do wyboru najnowszej wersji bez over-fetchu. */
|
|
27
|
+
getEntryVersionsMeta: GetEntryVersionsMeta;
|
|
28
|
+
/** Hydratacja `data` tylko dla wybranych par (entryId, versionNumber). */
|
|
29
|
+
getEntryVersionData: GetEntryVersionData;
|
|
26
30
|
deleteEntryVersion: DeleteEntryVersion;
|
|
27
31
|
createFormSubmission: CreateFormSubmission;
|
|
28
32
|
getFormSubmissions: GetFormSubmissions;
|
|
@@ -88,6 +92,14 @@ export type ArchiveEntry = (data: {
|
|
|
88
92
|
}) => Promise<DbEntry>;
|
|
89
93
|
export type CreateEntryVersion = (data: DbEntryVersionInsert) => Promise<DbEntryVersion>;
|
|
90
94
|
export type GetEntryVersions = (data: GetDbEntryVersionsOptions) => Promise<DbEntryVersion[]>;
|
|
95
|
+
export type GetEntryVersionsMeta = (data: GetDbEntryVersionsOptions) => Promise<DbEntryVersionMeta[]>;
|
|
96
|
+
export type GetEntryVersionData = (data: {
|
|
97
|
+
picks: {
|
|
98
|
+
entryId: string;
|
|
99
|
+
versionNumber: number;
|
|
100
|
+
}[];
|
|
101
|
+
lang: string;
|
|
102
|
+
}) => Promise<DbEntryVersion[]>;
|
|
91
103
|
export type UpdateEntryVersion = (data: {
|
|
92
104
|
id: string;
|
|
93
105
|
data: Partial<DbEntryVersion>;
|
package/dist/types/cms.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { DatabaseAdapter } from './adapters/db.js';
|
|
2
2
|
import type { FilesAdapter } from './adapters/files.js';
|
|
3
3
|
import type { CollectionConfig, CollectionConfigWithType } from './collections.js';
|
|
4
|
-
import type { Language } from './languages.js';
|
|
4
|
+
import type { Language, LanguageEntry } from './languages.js';
|
|
5
5
|
import type { SingleConfig, SingleConfigWithType } from './singles.js';
|
|
6
6
|
import type { CustomFieldDefinition, IconSetPlugin, Plugin } from './plugins.js';
|
|
7
7
|
import type { FormConfig } from './forms.js';
|
|
@@ -144,7 +144,8 @@ export interface AdminConfig {
|
|
|
144
144
|
extraNavItems?: import('../admin/types.js').AdminNavItem[];
|
|
145
145
|
}
|
|
146
146
|
export interface CMSConfig {
|
|
147
|
-
languages
|
|
147
|
+
/** Kod ISO albo obiekt `{ code, default?, url? }`. Znormalizowane do kodów w `CMS.languages`. */
|
|
148
|
+
languages: LanguageEntry[];
|
|
148
149
|
collections?: CollectionConfig[];
|
|
149
150
|
singles?: SingleConfig[];
|
|
150
151
|
forms?: FormConfig[];
|