includio-cms 0.7.2 → 0.13.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 (164) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/ROADMAP.md +40 -2
  3. package/dist/admin/api/generate-styles.d.ts +2 -0
  4. package/dist/admin/api/generate-styles.js +32 -0
  5. package/dist/admin/api/handler.js +33 -0
  6. package/dist/admin/api/media-gc.js +10 -4
  7. package/dist/admin/api/rest/handler.js +17 -0
  8. package/dist/admin/api/rest/routes/collections.js +25 -13
  9. package/dist/admin/api/rest/routes/entries.d.ts +1 -1
  10. package/dist/admin/api/rest/routes/entries.js +10 -10
  11. package/dist/admin/api/rest/routes/media.d.ts +2 -0
  12. package/dist/admin/api/rest/routes/media.js +9 -0
  13. package/dist/admin/api/rest/routes/schema.d.ts +5 -0
  14. package/dist/admin/api/rest/routes/schema.js +152 -0
  15. package/dist/admin/api/rest/routes/singletons.d.ts +1 -1
  16. package/dist/admin/api/rest/routes/singletons.js +8 -7
  17. package/dist/admin/api/rest/routes/upload.d.ts +2 -0
  18. package/dist/admin/api/rest/routes/upload.js +28 -0
  19. package/dist/admin/api/upload.js +13 -0
  20. package/dist/admin/client/collection/collection-entries.svelte +19 -6
  21. package/dist/admin/client/entry/entry.svelte +21 -23
  22. package/dist/admin/client/entry/header/a11y-validator.js +2 -2
  23. package/dist/admin/client/entry/header/publish-panel.svelte +33 -85
  24. package/dist/admin/client/entry/header/status-badge.svelte +2 -2
  25. package/dist/admin/client/entry/header/version-history-sheet.svelte +9 -9
  26. package/dist/admin/client/entry/header/visibility.svelte +16 -10
  27. package/dist/admin/client/entry/utils.d.ts +3 -0
  28. package/dist/admin/client/entry/utils.js +22 -4
  29. package/dist/admin/client/form/form-submission/form-submission-page.svelte +4 -1
  30. package/dist/admin/client/form/form-submission/submission-field.svelte +10 -0
  31. package/dist/admin/client/index.d.ts +1 -0
  32. package/dist/admin/client/index.js +1 -0
  33. package/dist/admin/client/maintenance/maintenance-page.svelte +146 -2
  34. package/dist/admin/components/fields/blocks-field.svelte +9 -10
  35. package/dist/admin/components/fields/field-renderer.svelte +4 -8
  36. package/dist/admin/components/fields/object-field.svelte +7 -12
  37. package/dist/admin/components/fields/select-field.svelte +8 -2
  38. package/dist/admin/components/fields/seo-field.svelte +40 -93
  39. package/dist/admin/components/fields/simple-array-field.svelte +5 -5
  40. package/dist/admin/components/fields/text-field-wrapper.svelte +52 -197
  41. package/dist/admin/components/fields/text-field-wrapper.svelte.d.ts +2 -2
  42. package/dist/admin/components/fields/url-field-wrapper.svelte +15 -25
  43. package/dist/admin/components/fields/url-field.svelte +61 -72
  44. package/dist/admin/components/media/file-upload.svelte +5 -1
  45. package/dist/admin/components/media/file-upload.svelte.d.ts +1 -0
  46. package/dist/admin/components/media/media-library.svelte +109 -37
  47. package/dist/admin/components/media/media-selector.svelte +79 -11
  48. package/dist/admin/components/media/tag-sidebar.svelte +10 -6
  49. package/dist/admin/components/media/tag-sidebar.svelte.d.ts +7 -2
  50. package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +21 -93
  51. package/dist/admin/components/tiptap/inline-block-node.js +6 -5
  52. package/dist/admin/components/tiptap/link-dialog.svelte +10 -11
  53. package/dist/admin/components/tiptap/slash-command.js +1 -1
  54. package/dist/admin/remote/entry.remote.d.ts +2 -5
  55. package/dist/admin/remote/entry.remote.js +22 -27
  56. package/dist/admin/remote/media.remote.d.ts +15 -0
  57. package/dist/admin/remote/media.remote.js +18 -2
  58. package/dist/admin/remote/preview.remote.js +3 -1
  59. package/dist/admin/utils/entryLabel.js +9 -6
  60. package/dist/admin/utils/translationStatus.js +1 -2
  61. package/dist/cli/scaffold/admin.js +34 -2
  62. package/dist/cms/runtime/api.d.ts +16 -12
  63. package/dist/cms/runtime/api.js +7 -6
  64. package/dist/cms/runtime/remote.js +2 -2
  65. package/dist/cms/runtime/schemas.d.ts +1 -1
  66. package/dist/cms/runtime/schemas.js +1 -1
  67. package/dist/cms/runtime/types.d.ts +118 -112
  68. package/dist/cms/runtime/types.js +0 -12
  69. package/dist/core/cms.d.ts +3 -1
  70. package/dist/core/cms.js +30 -0
  71. package/dist/core/fields/fieldSchemaToTs.js +9 -15
  72. package/dist/core/fields/formFieldSchemaToTs.js +7 -0
  73. package/dist/core/server/entries/operations/create.js +10 -4
  74. package/dist/core/server/entries/operations/get.d.ts +1 -0
  75. package/dist/core/server/entries/operations/get.js +186 -191
  76. package/dist/core/server/entries/operations/update.d.ts +6 -7
  77. package/dist/core/server/entries/operations/update.js +20 -38
  78. package/dist/core/server/fields/populateEntry.js +16 -52
  79. package/dist/core/server/fields/resolveImageFields.js +69 -120
  80. package/dist/core/server/fields/resolveRelationFields.js +30 -51
  81. package/dist/core/server/fields/resolveRichtextLinks.js +46 -100
  82. package/dist/core/server/fields/resolveTypographyOrphans.bench.d.ts +1 -0
  83. package/dist/core/server/fields/resolveTypographyOrphans.bench.js +87 -0
  84. package/dist/core/server/fields/resolveTypographyOrphans.d.ts +3 -0
  85. package/dist/core/server/fields/resolveTypographyOrphans.js +128 -0
  86. package/dist/core/server/fields/resolveUrlFields.js +47 -56
  87. package/dist/core/server/fields/utils/fixOrphans.d.ts +5 -0
  88. package/dist/core/server/fields/utils/fixOrphans.js +12 -0
  89. package/dist/core/server/fields/utils/imageStyles.d.ts +4 -2
  90. package/dist/core/server/fields/utils/imageStyles.js +41 -25
  91. package/dist/core/server/fields/utils/resolveMedia.js +1 -6
  92. package/dist/core/server/forms/submissions/operations/delete.js +26 -2
  93. package/dist/core/server/forms/submissions/utils/parseMultipart.d.ts +2 -0
  94. package/dist/core/server/forms/submissions/utils/parseMultipart.js +75 -0
  95. package/dist/core/server/generator/fields.d.ts +6 -0
  96. package/dist/core/server/generator/fields.js +43 -5
  97. package/dist/core/server/generator/formFieldSchemaToString.js +10 -0
  98. package/dist/core/server/generator/formFields.js +1 -0
  99. package/dist/core/server/generator/generator.js +98 -30
  100. package/dist/core/server/media/operations/getFiles.d.ts +5 -0
  101. package/dist/core/server/media/operations/getFiles.js +6 -0
  102. package/dist/core/server/media/operations/uploadPrivateFile.d.ts +4 -0
  103. package/dist/core/server/media/operations/uploadPrivateFile.js +8 -0
  104. package/dist/core/server/media/styles/operations/batchGenerateStyles.d.ts +16 -0
  105. package/dist/core/server/media/styles/operations/batchGenerateStyles.js +144 -0
  106. package/dist/db-postgres/index.js +303 -37
  107. package/dist/db-postgres/schema/entry.d.ts +0 -94
  108. package/dist/db-postgres/schema/entry.js +0 -6
  109. package/dist/db-postgres/schema/entryVersion.d.ts +17 -0
  110. package/dist/db-postgres/schema/entryVersion.js +1 -0
  111. package/dist/entity/index.d.ts +9 -4
  112. package/dist/entity/index.js +24 -24
  113. package/dist/files-local/index.js +43 -0
  114. package/dist/paraglide/messages/_index.d.ts +36 -3
  115. package/dist/paraglide/messages/_index.js +71 -3
  116. package/dist/paraglide/messages/en.d.ts +5 -0
  117. package/dist/paraglide/messages/en.js +14 -0
  118. package/dist/paraglide/messages/pl.d.ts +5 -0
  119. package/dist/paraglide/messages/pl.js +14 -0
  120. package/dist/sveltekit/components/preview.svelte +2 -326
  121. package/dist/sveltekit/components/preview.svelte.d.ts +5 -16
  122. package/dist/sveltekit/server/index.d.ts +2 -1
  123. package/dist/sveltekit/server/index.js +2 -1
  124. package/dist/sveltekit/server/preview.js +4 -7
  125. package/dist/types/adapters/db.d.ts +15 -1
  126. package/dist/types/adapters/files.d.ts +6 -0
  127. package/dist/types/cms.d.ts +5 -0
  128. package/dist/types/entries.d.ts +54 -18
  129. package/dist/types/fields.d.ts +14 -24
  130. package/dist/types/formFields.d.ts +7 -2
  131. package/dist/types/index.d.ts +2 -2
  132. package/dist/types/structured-content.d.ts +5 -0
  133. package/dist/updates/0.10.0/index.d.ts +2 -0
  134. package/dist/updates/0.10.0/index.js +15 -0
  135. package/dist/updates/0.11.0/index.d.ts +2 -0
  136. package/dist/updates/0.11.0/index.js +12 -0
  137. package/dist/updates/0.12.0/index.d.ts +2 -0
  138. package/dist/updates/0.12.0/index.js +12 -0
  139. package/dist/updates/0.13.0/index.d.ts +2 -0
  140. package/dist/updates/0.13.0/index.js +10 -0
  141. package/dist/updates/0.7.3/index.d.ts +2 -0
  142. package/dist/updates/0.7.3/index.js +10 -0
  143. package/dist/updates/0.8.0/index.d.ts +2 -0
  144. package/dist/updates/0.8.0/index.js +18 -0
  145. package/dist/updates/0.8.0/migrate.d.ts +2 -0
  146. package/dist/updates/0.8.0/migrate.js +101 -0
  147. package/dist/updates/0.9.0/index.d.ts +2 -0
  148. package/dist/updates/0.9.0/index.js +38 -0
  149. package/dist/updates/index.js +8 -1
  150. package/package.json +7 -6
  151. package/dist/admin/components/fields/image-field.svelte +0 -198
  152. package/dist/admin/components/fields/image-field.svelte.d.ts +0 -8
  153. package/dist/admin/components/fields/richtext-field.svelte +0 -13
  154. package/dist/admin/components/fields/richtext-field.svelte.d.ts +0 -8
  155. package/dist/admin/components/tiptap.svelte +0 -11
  156. package/dist/admin/components/tiptap.svelte.d.ts +0 -6
  157. package/dist/core/server/entries/utils/getEntryTranslation.d.ts +0 -1
  158. package/dist/core/server/entries/utils/getEntryTranslation.js +0 -18
  159. package/dist/paraglide/messages/hello_world.d.ts +0 -5
  160. package/dist/paraglide/messages/hello_world.js +0 -33
  161. package/dist/paraglide/messages/login_hello.d.ts +0 -16
  162. package/dist/paraglide/messages/login_hello.js +0 -34
  163. package/dist/paraglide/messages/login_please_login.d.ts +0 -16
  164. package/dist/paraglide/messages/login_please_login.js +0 -34
@@ -1,31 +1,7 @@
1
1
  import { getCMS } from '../../../cms.js';
2
2
  import { populateEntryData } from '../../fields/populateEntry.js';
3
3
  import { getFieldsFromConfig } from '../../../fields/layoutUtils.js';
4
- // Helper function to transform string values to localized objects
5
- function transformDataValuesToLocalized(dataValues, defaultLanguage) {
6
- const result = {};
7
- function transformValue(value) {
8
- if (typeof value === 'string') {
9
- // Transform string to localized object with default language
10
- return { [defaultLanguage]: value };
11
- }
12
- if (Array.isArray(value)) {
13
- return value.map(transformValue);
14
- }
15
- if (value && typeof value === 'object') {
16
- const transformed = {};
17
- Object.entries(value).forEach(([key, val]) => {
18
- transformed[key] = transformValue(val);
19
- });
20
- return transformed;
21
- }
22
- return value;
23
- }
24
- Object.entries(dataValues).forEach(([key, value]) => {
25
- result[key] = transformValue(value);
26
- });
27
- return result;
28
- }
4
+ import { getEntrySlugPath, getSlugFromEntryData, getEntryPath } from '../../fields/slugResolver.js';
29
5
  export const getDbEntries = async (options) => {
30
6
  return getCMS().databaseAdapter.getEntries(options);
31
7
  };
@@ -50,6 +26,33 @@ export const getDbEntryOrThrow = async (options) => {
50
26
  export const countRawEntries = async (options) => {
51
27
  return countDbEntries(options);
52
28
  };
29
+ /** Helper: group versions by lang into per-lang published/scheduled/draft maps */
30
+ function buildPerLangVersionMaps(versions) {
31
+ const now = new Date();
32
+ const publishedVersions = {};
33
+ const scheduledVersions = {};
34
+ const draftVersions = {};
35
+ // Group versions by lang
36
+ const byLang = new Map();
37
+ for (const v of versions) {
38
+ const arr = byLang.get(v.lang) || [];
39
+ arr.push(v);
40
+ byLang.set(v.lang, arr);
41
+ }
42
+ for (const [lang, langVersions] of byLang) {
43
+ const sorted = langVersions.sort((a, b) => b.versionNumber - a.versionNumber);
44
+ // Find latest published (publishedAt <= now)
45
+ const published = sorted.find((v) => v.publishedAt != null && v.publishedAt <= now) || null;
46
+ // Find scheduled (publishedAt > now)
47
+ const scheduled = sorted.find((v) => v.publishedAt != null && v.publishedAt > now) || null;
48
+ // Draft = latest version without publishedAt, or latest version newer than published
49
+ const draft = sorted.find((v) => v.publishedAt == null) || null;
50
+ publishedVersions[lang] = published;
51
+ scheduledVersions[lang] = scheduled;
52
+ draftVersions[lang] = draft;
53
+ }
54
+ return { publishedVersions, scheduledVersions, draftVersions };
55
+ }
53
56
  export const getRawEntries = async (options) => {
54
57
  const dbEntries = await getDbEntries(options);
55
58
  const entries = await Promise.all(dbEntries.map(async (entry) => {
@@ -57,31 +60,14 @@ export const getRawEntries = async (options) => {
57
60
  const versions = await getCMS().databaseAdapter.getEntryVersions({
58
61
  entryIds: [entry.id]
59
62
  });
60
- // Published/scheduled version from entry-level publishedVersionId
61
- const publishedOrScheduledVersion = entry.publishedVersionId
62
- ? versions.find((v) => v.id === entry.publishedVersionId) || null
63
- : null;
64
- // Determine if it's published or scheduled based on entry.publishedAt
65
- const isScheduled = publishedOrScheduledVersion &&
66
- entry.publishedAt &&
67
- entry.publishedAt > new Date();
68
- const publishedVersion = publishedOrScheduledVersion && !isScheduled
69
- ? publishedOrScheduledVersion
70
- : null;
71
- const scheduledVersion = publishedOrScheduledVersion && isScheduled
72
- ? publishedOrScheduledVersion
73
- : null;
74
- // Draft = latest non-published version by versionNumber
75
- const draftVersion = versions
76
- .filter((v) => v.id !== entry.publishedVersionId)
77
- .sort((a, b) => b.versionNumber - a.versionNumber)[0] || null;
63
+ const { publishedVersions, scheduledVersions, draftVersions } = buildPerLangVersionMaps(versions);
78
64
  return {
79
65
  ...entry,
80
66
  collection: getCMS().getBySlug(entry.slug),
81
67
  versions: versions.sort((a, b) => b.versionNumber - a.versionNumber),
82
- publishedVersion,
83
- scheduledVersion,
84
- draftVersion
68
+ publishedVersions,
69
+ scheduledVersions,
70
+ draftVersions
85
71
  };
86
72
  }
87
73
  catch {
@@ -106,60 +92,56 @@ export const getRawEntryOrThrow = async (options) => {
106
92
  }
107
93
  return entry;
108
94
  };
109
- function getNestedValue(obj, keys) {
110
- let current = obj;
111
- for (const key of keys) {
112
- if (current === null || current === undefined || typeof current !== 'object')
113
- return undefined;
114
- current = current[key];
115
- }
116
- return current;
117
- }
118
- function matchesDataValues(data, filter, prefix = []) {
119
- for (const [key, value] of Object.entries(filter)) {
120
- const path = [...prefix, key];
121
- if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
122
- if (!matchesDataValues(data, value, path))
123
- return false;
124
- }
125
- else {
126
- const actual = getNestedValue(data, path);
127
- if (actual !== value)
128
- return false;
129
- }
130
- }
131
- return true;
132
- }
133
- function matchesDataLike(data, filter, prefix = []) {
134
- for (const [key, value] of Object.entries(filter)) {
135
- const path = [...prefix, key];
136
- if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
137
- if (!matchesDataLike(data, value, path))
138
- return false;
139
- }
140
- else if (typeof value === 'string') {
141
- const actual = getNestedValue(data, path);
142
- if (typeof actual !== 'string')
143
- return false;
144
- if (!actual.toLowerCase().includes(value.toLowerCase()))
145
- return false;
146
- }
147
- }
148
- return true;
149
- }
150
95
  export const getEntries = async (options = {}) => {
151
- const language = options.language || getCMS().languages[0];
96
+ const cms = getCMS();
97
+ const language = options.language || cms.languages[0];
152
98
  const status = options.status || 'published';
153
- const dataValues = options.dataValues
154
- ? transformDataValuesToLocalized(options.dataValues, language)
155
- : undefined;
156
- const dataLike = options.dataLike
157
- ? transformDataValuesToLocalized(options.dataLike, language)
158
- : undefined;
99
+ // Fast path: DB-level pagination when limit/offset provided and adapter supports it
100
+ if (options.limit != null && cms.databaseAdapter.getPaginatedEntries) {
101
+ const rows = await cms.databaseAdapter.getPaginatedEntries({
102
+ slug: options.slug,
103
+ ids: options.ids,
104
+ language,
105
+ status,
106
+ dataValues: options.dataValues,
107
+ dataLike: options.dataLike,
108
+ dataILikeOr: options.dataILikeOr,
109
+ orderBy: options.orderBy,
110
+ dataOrderBy: options.dataOrderBy,
111
+ limit: options.limit,
112
+ offset: options.offset ?? 0
113
+ });
114
+ const entries = await Promise.all(rows.map(async ({ entry, version }) => {
115
+ try {
116
+ const config = cms.getBySlug(entry.slug);
117
+ const fields = getFieldsFromConfig(config);
118
+ const populatedData = await populateEntryData(version.data, fields, language);
119
+ const slugPath = getEntrySlugPath(entry.slug);
120
+ const slug = getSlugFromEntryData(version.data, slugPath, language);
121
+ const _url = slug ? getEntryPath(entry.slug, slug) : undefined;
122
+ return {
123
+ _id: entry.id,
124
+ _slug: entry.slug,
125
+ _type: entry.type,
126
+ _publishedAt: version.publishedAt,
127
+ _url,
128
+ ...populatedData
129
+ };
130
+ }
131
+ catch (error) {
132
+ console.error(`[CMS] Failed to populate entry ${entry.id} (${entry.slug}):`, error);
133
+ return null;
134
+ }
135
+ }));
136
+ return entries.filter((e) => e !== null);
137
+ }
138
+ // Slow path: in-memory pagination (backward compat)
159
139
  const ids = options.ids;
160
140
  const slug = options.slug;
161
- // Get entries that match the slug/ids filter
162
- const dbEntries = await getCMS().databaseAdapter.getEntries({
141
+ const dataValues = options.dataValues;
142
+ const dataLike = options.dataLike;
143
+ const dataILikeOr = options.dataILikeOr;
144
+ const dbEntries = await cms.databaseAdapter.getEntries({
163
145
  ids,
164
146
  slug,
165
147
  orderBy: options.orderBy
@@ -167,87 +149,70 @@ export const getEntries = async (options = {}) => {
167
149
  if (dbEntries.length === 0) {
168
150
  return [];
169
151
  }
170
- // Filter entries by status at entry level
152
+ const filteredEntries = status === 'archived'
153
+ ? dbEntries.filter((e) => e.archivedAt != null)
154
+ : dbEntries.filter((e) => e.archivedAt == null);
155
+ if (filteredEntries.length === 0) {
156
+ return [];
157
+ }
158
+ const entriesMap = new Map(filteredEntries.map((entry) => [entry.id, entry]));
159
+ const entryIds = filteredEntries.map((entry) => entry.id);
160
+ const allVersions = await cms.databaseAdapter.getEntryVersions({
161
+ entryIds,
162
+ lang: language,
163
+ dataValues,
164
+ dataLike,
165
+ dataILikeOr
166
+ });
171
167
  const now = new Date();
172
- const filteredEntries = dbEntries.filter((entry) => {
168
+ const versionEntries = [];
169
+ const versionsByEntry = new Map();
170
+ for (const v of allVersions) {
171
+ const arr = versionsByEntry.get(v.entryId) || [];
172
+ arr.push(v);
173
+ versionsByEntry.set(v.entryId, arr);
174
+ }
175
+ for (const [entryId, versions] of versionsByEntry) {
176
+ const dbEntry = entriesMap.get(entryId);
177
+ if (!dbEntry)
178
+ continue;
179
+ const sorted = versions.sort((a, b) => b.versionNumber - a.versionNumber);
180
+ let picked = null;
173
181
  switch (status) {
174
182
  case 'published':
175
- return entry.publishedVersionId != null && entry.publishedAt != null && entry.publishedAt <= now;
183
+ picked = sorted.find((v) => v.publishedAt != null && v.publishedAt <= now) || null;
184
+ break;
176
185
  case 'scheduled':
177
- return entry.publishedVersionId != null && entry.publishedAt != null && entry.publishedAt > now;
186
+ picked = sorted.find((v) => v.publishedAt != null && v.publishedAt > now) || null;
187
+ break;
178
188
  case 'draft':
179
- return entry.publishedVersionId == null;
189
+ picked = sorted.find((v) => v.publishedAt == null) || null;
190
+ break;
180
191
  case 'archived':
181
- return entry.archivedAt != null;
182
- default:
183
- return true;
192
+ picked = sorted[0] || null;
193
+ break;
184
194
  }
185
- });
186
- if (filteredEntries.length === 0) {
187
- return [];
188
- }
189
- // Build entries map for quick lookup
190
- const entriesMap = new Map(filteredEntries.map((entry) => [entry.id, entry]));
191
- const entryIds = filteredEntries.map((entry) => entry.id);
192
- // For published/scheduled: get the specific published version
193
- // For draft: get latest version (highest versionNumber)
194
- let versionEntries;
195
- if (status === 'published' || status === 'scheduled') {
196
- // Get published versions by their IDs
197
- const publishedVersionIds = filteredEntries
198
- .map((e) => e.publishedVersionId)
199
- .filter((id) => id != null);
200
- if (publishedVersionIds.length === 0)
201
- return [];
202
- const versions = await getCMS().databaseAdapter.getEntryVersions({
203
- ids: publishedVersionIds
204
- });
205
- versionEntries = versions
206
- .map((v) => {
207
- const dbEntry = entriesMap.get(v.entryId);
208
- if (!dbEntry)
209
- return null;
210
- return { version: v, dbEntry };
211
- })
212
- .filter((e) => e != null);
213
- }
214
- else {
215
- // Draft/archived: get all versions and pick latest
216
- const allVersions = await getCMS().databaseAdapter.getEntryVersions({
217
- entryIds
218
- });
219
- const latestByEntry = new Map();
220
- for (const version of allVersions) {
221
- const dbEntry = entriesMap.get(version.entryId);
222
- if (!dbEntry)
223
- continue;
224
- const existing = latestByEntry.get(version.entryId);
225
- if (!existing || version.versionNumber > existing.version.versionNumber) {
226
- latestByEntry.set(version.entryId, { version, dbEntry });
227
- }
195
+ if (picked) {
196
+ versionEntries.push({ version: picked, dbEntry });
228
197
  }
229
- versionEntries = Array.from(latestByEntry.values());
230
198
  }
231
- // Re-sort to match original entry order (DB returns sorted by sortOrder, but version fetch may reorder)
232
199
  const entryOrder = new Map(filteredEntries.map((e, i) => [e.id, i]));
233
200
  versionEntries.sort((a, b) => (entryOrder.get(a.dbEntry.id) ?? 0) - (entryOrder.get(b.dbEntry.id) ?? 0));
234
- // Post-filter: apply dataValues/dataLike on versions
235
- if (dataValues) {
236
- versionEntries = versionEntries.filter((e) => matchesDataValues(e.version.data, dataValues));
237
- }
238
- if (dataLike) {
239
- versionEntries = versionEntries.filter((e) => matchesDataLike(e.version.data, dataLike));
240
- }
241
- // Process entries in parallel
242
201
  const entries = await Promise.all(versionEntries.map(async ({ version, dbEntry }) => {
243
202
  try {
244
- const config = getCMS().getBySlug(dbEntry.slug);
203
+ const config = cms.getBySlug(dbEntry.slug);
204
+ const fields = getFieldsFromConfig(config);
205
+ const populatedData = await populateEntryData(version.data, fields, language);
206
+ const slugPath = getEntrySlugPath(dbEntry.slug);
207
+ const slug = getSlugFromEntryData(version.data, slugPath, language);
208
+ const _url = slug ? getEntryPath(dbEntry.slug, slug) : undefined;
245
209
  return {
246
- ...dbEntry,
247
- data: await populateEntryData(version.data, getFieldsFromConfig(config), language),
248
- status,
249
- publishedAt: dbEntry.publishedAt,
250
- populated: true
210
+ _id: dbEntry.id,
211
+ _slug: dbEntry.slug,
212
+ _type: dbEntry.type,
213
+ _publishedAt: version.publishedAt,
214
+ _url,
215
+ ...populatedData
251
216
  };
252
217
  }
253
218
  catch (error) {
@@ -255,7 +220,31 @@ export const getEntries = async (options = {}) => {
255
220
  return null;
256
221
  }
257
222
  }));
258
- return entries.filter((e) => e !== null);
223
+ let results = entries.filter((e) => e !== null);
224
+ if (options.offset)
225
+ results = results.slice(options.offset);
226
+ if (options.limit)
227
+ results = results.slice(0, options.limit);
228
+ return results;
229
+ };
230
+ export const countEntries = async (options) => {
231
+ const cms = getCMS();
232
+ const language = options.language || cms.languages[0];
233
+ const status = options.status || 'published';
234
+ if (cms.databaseAdapter.countPaginatedEntries) {
235
+ return cms.databaseAdapter.countPaginatedEntries({
236
+ slug: options.slug,
237
+ ids: options.ids,
238
+ language,
239
+ status,
240
+ dataValues: options.dataValues,
241
+ dataLike: options.dataLike,
242
+ dataILikeOr: options.dataILikeOr
243
+ });
244
+ }
245
+ // Fallback: use old getEntries without limit/offset (counts all)
246
+ const entries = await getEntries({ ...options });
247
+ return entries.length;
259
248
  };
260
249
  export const getEntry = async (options = {}) => {
261
250
  const [entry] = await getEntries({
@@ -282,41 +271,38 @@ export const getEntryLabels = async (options) => {
282
271
  });
283
272
  if (dbEntries.length === 0)
284
273
  return [];
285
- // Filter by status at entry level
274
+ const entryIds = dbEntries.map((e) => e.id);
275
+ const language = cms.languages[0];
276
+ // Get versions for default language
277
+ const allVersions = await cms.databaseAdapter.getEntryVersions({
278
+ entryIds,
279
+ lang: language
280
+ });
286
281
  const now = new Date();
287
282
  const statusFilter = options.status ?? 'all';
288
- const filteredDbEntries = statusFilter === 'all'
289
- ? dbEntries
290
- : dbEntries.filter((entry) => {
291
- if (statusFilter === 'published') {
292
- return entry.publishedVersionId != null && entry.publishedAt != null && entry.publishedAt <= now;
293
- }
294
- // draft
295
- return entry.publishedVersionId == null;
296
- });
297
- if (filteredDbEntries.length === 0)
298
- return [];
299
- const entryIds = filteredDbEntries.map((e) => e.id);
300
- const allVersions = await cms.databaseAdapter.getEntryVersions({ entryIds });
301
- const language = cms.languages[0];
302
283
  const entryAdminTitle = config.entryAdminTitle;
303
- let results = filteredDbEntries.map((entry) => {
304
- // Pick published version or latest draft
284
+ let results = dbEntries.map((entry) => {
305
285
  const entryVersions = allVersions.filter((v) => v.entryId === entry.id);
306
- const publishedVersion = entry.publishedVersionId
307
- ? entryVersions.find((v) => v.id === entry.publishedVersionId)
308
- : null;
309
- const latestVersion = publishedVersion ??
310
- entryVersions.sort((a, b) => b.versionNumber - a.versionNumber)[0] ?? null;
286
+ const sorted = entryVersions.sort((a, b) => b.versionNumber - a.versionNumber);
287
+ // Determine status from versions
288
+ const publishedVersion = sorted.find((v) => v.publishedAt != null && v.publishedAt <= now) || null;
289
+ const hasPublished = publishedVersion != null;
290
+ // Filter by status
291
+ if (statusFilter === 'published' && !hasPublished)
292
+ return null;
293
+ if (statusFilter === 'draft' && hasPublished)
294
+ return null;
295
+ const latestVersion = publishedVersion ?? sorted[0] ?? null;
311
296
  let label = entry.id;
312
297
  if (entryAdminTitle && latestVersion) {
313
298
  const titleData = latestVersion.data[entryAdminTitle];
314
- if (titleData && typeof titleData === 'object') {
315
- label = String(titleData[language] || Object.values(titleData)[0] || entry.id);
299
+ // Data is now flat — titleData is the string directly
300
+ if (typeof titleData === 'string') {
301
+ label = titleData || entry.id;
316
302
  }
317
303
  }
318
304
  return { id: entry.id, label };
319
- });
305
+ }).filter((r) => r != null);
320
306
  // Post-query search filtering (case-insensitive includes)
321
307
  if (options.search) {
322
308
  const searchLower = options.search.toLowerCase();
@@ -358,9 +344,18 @@ export const getEntryVersion = async (options) => {
358
344
  }
359
345
  try {
360
346
  const config = getCMS().getBySlug(dbEntry.slug);
347
+ const fields = getFieldsFromConfig(config);
348
+ const populatedData = await populateEntryData(dbEntryVersion.data, fields, language);
349
+ const slugPath = getEntrySlugPath(dbEntry.slug);
350
+ const entrySlug = getSlugFromEntryData(dbEntryVersion.data, slugPath, language);
351
+ const _url = entrySlug ? getEntryPath(dbEntry.slug, entrySlug) : undefined;
361
352
  return {
362
- ...dbEntry,
363
- data: await populateEntryData(dbEntryVersion.data, getFieldsFromConfig(config), language)
353
+ _id: dbEntry.id,
354
+ _slug: dbEntry.slug,
355
+ _type: dbEntry.type,
356
+ _publishedAt: dbEntryVersion.publishedAt,
357
+ _url,
358
+ ...populatedData
364
359
  };
365
360
  }
366
361
  catch (error) {
@@ -1,11 +1,7 @@
1
1
  import type { DbEntry, DbEntryVersion } from '../../../../types/entries.js';
2
2
  import z from 'zod';
3
3
  export declare const updateEntrySchema: z.ZodObject<{
4
- availableLocales: z.ZodOptional<z.ZodArray<z.ZodString>>;
5
4
  archivedAt: z.ZodOptional<z.ZodNullable<z.ZodDate>>;
6
- publishedAt: z.ZodOptional<z.ZodNullable<z.ZodDate>>;
7
- publishedVersionId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
8
- publishedBy: z.ZodOptional<z.ZodNullable<z.ZodString>>;
9
5
  sortOrder: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
10
6
  }, z.z.core.$strip>;
11
7
  export declare const updateEntry: (id: string, data: Partial<DbEntry>) => Promise<DbEntry>;
@@ -17,8 +13,11 @@ export declare const updateEntryVersionSchema: z.ZodObject<{
17
13
  export declare const updateEntryVersion: (id: string, data: Partial<DbEntryVersion>) => Promise<DbEntryVersion>;
18
14
  export declare const updateEntryVersionCommandTypes: readonly ["draft", "published-now", "published-scheduled", "cancel-published"];
19
15
  export type UpdateEntryVersionCommandType = (typeof updateEntryVersionCommandTypes)[number];
20
- export declare const pruneOldDraftVersions: (entryId: string) => Promise<void>;
21
- export declare const upsertDraftVersion: (entryId: string, data: Record<string, unknown>, options?: {
16
+ /** Prune old draft versions scoped to (entryId, lang) */
17
+ export declare const pruneOldDraftVersions: (entryId: string, lang: string) => Promise<void>;
18
+ /** Upsert draft version scoped to (entryId, lang) */
19
+ export declare const upsertDraftVersion: (entryId: string, data: Record<string, unknown>, lang: string, options?: {
22
20
  skipValidation?: boolean;
23
21
  }) => Promise<DbEntryVersion>;
24
- export declare const unpublishEntry: (entryId: string) => Promise<void>;
22
+ /** Unpublish a specific language version for an entry */
23
+ export declare const unpublishEntryLang: (entryId: string, lang: string) => Promise<void>;
@@ -5,11 +5,7 @@ import z from 'zod';
5
5
  import { getDbEntryOrThrow, getDbEntryVersionOrThrow, getDbEntryVersions } from './get.js';
6
6
  import { createEntryVersion } from './create.js';
7
7
  export const updateEntrySchema = z.object({
8
- availableLocales: z.array(z.string()).optional(),
9
8
  archivedAt: z.date().nullable().optional(),
10
- publishedAt: z.date().nullable().optional(),
11
- publishedVersionId: z.string().uuid().nullable().optional(),
12
- publishedBy: z.string().nullable().optional(),
13
9
  sortOrder: z.number().int().nullable().optional()
14
10
  });
15
11
  export const updateEntry = async (id, data) => {
@@ -19,21 +15,9 @@ export const updateEntry = async (id, data) => {
19
15
  }
20
16
  const filteredData = filteredDataParse.data;
21
17
  const dataToUpdate = {};
22
- if (filteredData.availableLocales !== undefined) {
23
- dataToUpdate.availableLocales = filteredData.availableLocales;
24
- }
25
18
  if (filteredData.archivedAt !== undefined) {
26
19
  dataToUpdate.archivedAt = filteredData.archivedAt;
27
20
  }
28
- if (filteredData.publishedAt !== undefined) {
29
- dataToUpdate.publishedAt = filteredData.publishedAt;
30
- }
31
- if (filteredData.publishedVersionId !== undefined) {
32
- dataToUpdate.publishedVersionId = filteredData.publishedVersionId;
33
- }
34
- if (filteredData.publishedBy !== undefined) {
35
- dataToUpdate.publishedBy = filteredData.publishedBy;
36
- }
37
21
  if (filteredData.sortOrder !== undefined) {
38
22
  dataToUpdate.sortOrder = filteredData.sortOrder;
39
23
  }
@@ -63,7 +47,10 @@ export const updateEntryVersion = async (id, data) => {
63
47
  const entry = await getDbEntryOrThrow({ id: version.entryId });
64
48
  const config = getCMS().getBySlug(entry.slug);
65
49
  const languages = getCMS().languages;
66
- const schema = generateZodSchemaFromFields(getFieldsFromConfig(config), languages);
50
+ // Validate with localized: false — data is flat single-language
51
+ const schema = generateZodSchemaFromFields(getFieldsFromConfig(config), languages, {
52
+ localized: false
53
+ });
67
54
  const parsedData = schema.safeParse(filteredData.data);
68
55
  if (!parsedData.success) {
69
56
  throw Error('Invalid data: ' + parsedData.error.flatten());
@@ -92,28 +79,27 @@ export const updateEntryVersionCommandTypes = [
92
79
  'cancel-published'
93
80
  ];
94
81
  const MAX_DRAFT_VERSIONS = 10;
95
- export const pruneOldDraftVersions = async (entryId) => {
96
- const entry = await getDbEntryOrThrow({ id: entryId });
82
+ /** Prune old draft versions scoped to (entryId, lang) */
83
+ export const pruneOldDraftVersions = async (entryId, lang) => {
97
84
  const versions = await getCMS().databaseAdapter.getEntryVersions({
98
- entryIds: [entryId]
85
+ entryIds: [entryId],
86
+ lang
99
87
  });
100
88
  // Sort by version number desc
101
89
  const sorted = versions.sort((a, b) => b.versionNumber - a.versionNumber);
102
- // Published/scheduled version is identified by entry.publishedVersionId
103
- const publishedVersionId = entry.publishedVersionId;
104
- const draftVersions = sorted.filter((v) => v.id !== publishedVersionId);
105
- // Keep published version + up to MAX_DRAFT_VERSIONS drafts
90
+ // Keep published versions + up to MAX_DRAFT_VERSIONS drafts
91
+ const draftVersions = sorted.filter((v) => v.publishedAt == null);
106
92
  if (draftVersions.length > MAX_DRAFT_VERSIONS) {
107
93
  const toDelete = draftVersions.slice(MAX_DRAFT_VERSIONS);
108
94
  await Promise.all(toDelete.map((v) => getCMS().databaseAdapter.deleteEntryVersion({ id: v.id })));
109
95
  }
110
96
  };
111
- export const upsertDraftVersion = async (entryId, data, options) => {
112
- const entry = await getDbEntryOrThrow({ id: entryId });
113
- const versions = await getDbEntryVersions({ entryIds: [entryId] });
114
- // Sort desc by versionNumber, find latest non-published draft
97
+ /** Upsert draft version scoped to (entryId, lang) */
98
+ export const upsertDraftVersion = async (entryId, data, lang, options) => {
99
+ const versions = await getDbEntryVersions({ entryIds: [entryId], lang });
100
+ // Sort desc by versionNumber, find latest unpublished draft
115
101
  const sorted = versions.sort((a, b) => b.versionNumber - a.versionNumber);
116
- const latestDraft = sorted.find((v) => v.id !== entry.publishedVersionId);
102
+ const latestDraft = sorted.find((v) => v.publishedAt == null);
117
103
  if (latestDraft) {
118
104
  // Compare data — if identical, skip
119
105
  const existingData = JSON.stringify(latestDraft.data);
@@ -132,17 +118,13 @@ export const upsertDraftVersion = async (entryId, data, options) => {
132
118
  return updated;
133
119
  }
134
120
  // No draft exists — create new version
135
- return createEntryVersion({ entryId, data }, options);
121
+ return createEntryVersion({ entryId, lang, data }, options);
136
122
  };
137
- export const unpublishEntry = async (entryId) => {
138
- await updateEntry(entryId, {
139
- publishedVersionId: null,
140
- publishedAt: null,
141
- publishedBy: null
142
- });
143
- // Dual-write: clear version-level publishedAt for backward compat
123
+ /** Unpublish a specific language version for an entry */
124
+ export const unpublishEntryLang = async (entryId, lang) => {
144
125
  const versions = await getCMS().databaseAdapter.getEntryVersions({
145
- entryIds: [entryId]
126
+ entryIds: [entryId],
127
+ lang
146
128
  });
147
129
  await Promise.all(versions
148
130
  .filter((v) => v.publishedAt !== null)