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
@@ -0,0 +1,144 @@
1
+ import { getCMS } from '../../../../cms.js';
2
+ import { isProcessableImage, defaultStyles, expandStyleFormats, getOriginalFormat } from '../../../fields/utils/imageStyles.js';
3
+ import { getImageStyle } from './getImageStyle.js';
4
+ const IMAGE_MIME_TYPES = [
5
+ 'image/jpeg',
6
+ 'image/png',
7
+ 'image/webp',
8
+ 'image/avif',
9
+ 'image/gif',
10
+ 'image/tiff'
11
+ ];
12
+ async function generateStylesForFile(file) {
13
+ const cms = getCMS();
14
+ const origFormat = getOriginalFormat(file);
15
+ const expanded = expandStyleFormats(defaultStyles, origFormat);
16
+ let created = 0;
17
+ let skipped = 0;
18
+ for (const style of expanded) {
19
+ const existing = await cms.databaseAdapter.getImageStyle(file.id, style);
20
+ if (existing) {
21
+ skipped++;
22
+ }
23
+ else {
24
+ await getImageStyle(file.id, style);
25
+ created++;
26
+ }
27
+ if (style.srcset && file.width) {
28
+ const widths = style.srcset.filter((w) => w <= file.width);
29
+ for (const w of widths) {
30
+ const widthStyle = {
31
+ ...style,
32
+ name: `${style.name}_${w}w`,
33
+ width: w,
34
+ srcset: undefined,
35
+ sizes: undefined
36
+ };
37
+ const existingW = await cms.databaseAdapter.getImageStyle(file.id, widthStyle);
38
+ if (existingW) {
39
+ skipped++;
40
+ }
41
+ else {
42
+ await getImageStyle(file.id, widthStyle);
43
+ created++;
44
+ }
45
+ }
46
+ }
47
+ }
48
+ return { created, skipped };
49
+ }
50
+ export async function* batchGenerateAllStyles(signal) {
51
+ const allFiles = await getCMS().databaseAdapter.getMediaFiles({
52
+ data: { mimeTypes: IMAGE_MIME_TYPES }
53
+ });
54
+ const files = allFiles.filter(isProcessableImage);
55
+ const total = files.length;
56
+ let totalCreated = 0;
57
+ let totalSkipped = 0;
58
+ for (let i = 0; i < files.length; i++) {
59
+ if (signal?.aborted)
60
+ return;
61
+ const file = files[i];
62
+ yield {
63
+ type: 'progress',
64
+ total,
65
+ processed: i,
66
+ created: totalCreated,
67
+ skipped: totalSkipped,
68
+ currentFile: file.name
69
+ };
70
+ try {
71
+ const { created, skipped } = await generateStylesForFile(file);
72
+ totalCreated += created;
73
+ totalSkipped += skipped;
74
+ }
75
+ catch (e) {
76
+ yield {
77
+ type: 'error',
78
+ total,
79
+ processed: i + 1,
80
+ created: totalCreated,
81
+ skipped: totalSkipped,
82
+ currentFile: file.name,
83
+ error: e instanceof Error ? e.message : String(e)
84
+ };
85
+ continue;
86
+ }
87
+ }
88
+ yield { type: 'done', total, processed: total, created: totalCreated, skipped: totalSkipped };
89
+ }
90
+ function getExpectedStyleNames(file) {
91
+ const origFormat = getOriginalFormat(file);
92
+ const expanded = expandStyleFormats(defaultStyles, origFormat);
93
+ const names = [];
94
+ for (const style of expanded) {
95
+ names.push(style.name);
96
+ if (style.srcset && file.width) {
97
+ for (const w of style.srcset.filter((sw) => sw <= file.width)) {
98
+ names.push(`${style.name}_${w}w`);
99
+ }
100
+ }
101
+ }
102
+ return names;
103
+ }
104
+ export async function getStylesStatus() {
105
+ const cms = getCMS();
106
+ const [allFiles, allStyles] = await Promise.all([
107
+ cms.databaseAdapter.getMediaFiles({ data: { mimeTypes: IMAGE_MIME_TYPES } }),
108
+ cms.databaseAdapter.getAllImageStyles()
109
+ ]);
110
+ const files = allFiles.filter(isProcessableImage);
111
+ // Group existing styles by mediaFileId → set of style names
112
+ const existingByFile = new Map();
113
+ for (const s of allStyles) {
114
+ const fileId = s.mediaFileId;
115
+ if (!existingByFile.has(fileId))
116
+ existingByFile.set(fileId, new Set());
117
+ // We only have id/url/mediaFileId from getAllImageStyles, not name.
118
+ // So we count per-file total instead
119
+ }
120
+ // Count expected per-file and compare with existing count per-file
121
+ let expectedStyles = 0;
122
+ let missingStyles = 0;
123
+ // Count existing per mediaFileId
124
+ const existingCountByFile = new Map();
125
+ for (const s of allStyles) {
126
+ const fileId = s.mediaFileId;
127
+ existingCountByFile.set(fileId, (existingCountByFile.get(fileId) ?? 0) + 1);
128
+ }
129
+ for (const file of files) {
130
+ const expectedNames = getExpectedStyleNames(file);
131
+ const expected = expectedNames.length;
132
+ const existing = existingCountByFile.get(file.id) ?? 0;
133
+ expectedStyles += expected;
134
+ if (existing < expected) {
135
+ missingStyles += expected - existing;
136
+ }
137
+ }
138
+ return {
139
+ processableImages: files.length,
140
+ expectedStyles,
141
+ existingStyles: allStyles.length,
142
+ missingStyles
143
+ };
144
+ }
@@ -56,6 +56,142 @@ function buildJsonLikeConditions(dataLike) {
56
56
  });
57
57
  return conditions;
58
58
  }
59
+ // Helper function to build JSON path conditions (ILIKE match, OR logic)
60
+ function buildJsonILikeOrConditions(dataILikeOr) {
61
+ const conditions = [];
62
+ function processValue(value, path = []) {
63
+ if (value === null || value === undefined)
64
+ return;
65
+ if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
66
+ Object.entries(value).forEach(([key, nestedValue]) => {
67
+ processValue(nestedValue, [...path, key]);
68
+ });
69
+ }
70
+ else {
71
+ validateJsonPathKeys(path);
72
+ const jsonPath = path.join(',');
73
+ conditions.push(sql `${schema.entryVersionsTable.data}#>>'{${sql.raw(jsonPath)}}' ILIKE ${`%${String(value)}%`}`);
74
+ }
75
+ }
76
+ Object.entries(dataILikeOr).forEach(([key, value]) => {
77
+ processValue(value, [key]);
78
+ });
79
+ if (conditions.length === 0)
80
+ return undefined;
81
+ if (conditions.length === 1)
82
+ return conditions[0];
83
+ return sql `(${sql.join(conditions, sql ` OR `)})`;
84
+ }
85
+ // Raw SQL condition builders for CTE queries (use v.column / e.column aliases)
86
+ function buildRawJsonConditions(dataValue) {
87
+ const conditions = [];
88
+ function processValue(value, path = []) {
89
+ if (value === null || value === undefined)
90
+ return;
91
+ if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
92
+ Object.entries(value).forEach(([key, nestedValue]) => {
93
+ processValue(nestedValue, [...path, key]);
94
+ });
95
+ }
96
+ else {
97
+ validateJsonPathKeys(path);
98
+ const jsonPath = path.join(',');
99
+ conditions.push(sql `v.data#>>'{${sql.raw(jsonPath)}}' = ${String(value)}`);
100
+ }
101
+ }
102
+ Object.entries(dataValue).forEach(([key, value]) => {
103
+ processValue(value, [key]);
104
+ });
105
+ return conditions;
106
+ }
107
+ function buildRawJsonLikeConditions(dataLike) {
108
+ const conditions = [];
109
+ function processValue(value, path = []) {
110
+ if (value === null || value === undefined)
111
+ return;
112
+ if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
113
+ Object.entries(value).forEach(([key, nestedValue]) => {
114
+ processValue(nestedValue, [...path, key]);
115
+ });
116
+ }
117
+ else {
118
+ validateJsonPathKeys(path);
119
+ const jsonPath = path.join(',');
120
+ conditions.push(sql `v.data#>>'{${sql.raw(jsonPath)}}' LIKE ${`%${String(value)}%`}`);
121
+ }
122
+ }
123
+ Object.entries(dataLike).forEach(([key, value]) => {
124
+ processValue(value, [key]);
125
+ });
126
+ return conditions;
127
+ }
128
+ function buildRawJsonILikeOrConditions(dataILikeOr) {
129
+ const conditions = [];
130
+ function processValue(value, path = []) {
131
+ if (value === null || value === undefined)
132
+ return;
133
+ if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
134
+ Object.entries(value).forEach(([key, nestedValue]) => {
135
+ processValue(nestedValue, [...path, key]);
136
+ });
137
+ }
138
+ else {
139
+ validateJsonPathKeys(path);
140
+ const jsonPath = path.join(',');
141
+ conditions.push(sql `v.data#>>'{${sql.raw(jsonPath)}}' ILIKE ${`%${String(value)}%`}`);
142
+ }
143
+ }
144
+ Object.entries(dataILikeOr).forEach(([key, value]) => {
145
+ processValue(value, [key]);
146
+ });
147
+ if (conditions.length === 0)
148
+ return undefined;
149
+ if (conditions.length === 1)
150
+ return conditions[0];
151
+ return sql `(${sql.join(conditions, sql ` OR `)})`;
152
+ }
153
+ function buildCteVersionConditions(options) {
154
+ const conditions = [sql `v.lang = ${options.language}`];
155
+ if (options.status === 'published') {
156
+ conditions.push(sql `v.published_at IS NOT NULL`);
157
+ conditions.push(sql `v.published_at <= NOW()`);
158
+ }
159
+ else if (options.status === 'draft') {
160
+ conditions.push(sql `v.published_at IS NULL`);
161
+ }
162
+ else if (options.status === 'scheduled') {
163
+ conditions.push(sql `v.published_at IS NOT NULL`);
164
+ conditions.push(sql `v.published_at > NOW()`);
165
+ }
166
+ if (options.dataValues) {
167
+ conditions.push(...buildRawJsonConditions(options.dataValues));
168
+ }
169
+ if (options.dataLike) {
170
+ conditions.push(...buildRawJsonLikeConditions(options.dataLike));
171
+ }
172
+ if (options.dataILikeOr) {
173
+ const orCondition = buildRawJsonILikeOrConditions(options.dataILikeOr);
174
+ if (orCondition)
175
+ conditions.push(orCondition);
176
+ }
177
+ return conditions;
178
+ }
179
+ function buildCteEntryConditions(options) {
180
+ const conditions = [];
181
+ if (options.status === 'archived') {
182
+ conditions.push(sql `e.archived_at IS NOT NULL`);
183
+ }
184
+ else {
185
+ conditions.push(sql `e.archived_at IS NULL`);
186
+ }
187
+ if (options.slug) {
188
+ conditions.push(sql `e.slug = ${options.slug}`);
189
+ }
190
+ if (options.ids && options.ids.length > 0) {
191
+ conditions.push(sql `e.id = ANY(${options.ids})`);
192
+ }
193
+ return conditions;
194
+ }
59
195
  /** Hydrate tags onto raw media file rows */
60
196
  async function hydrateFileTags(db, files) {
61
197
  if (files.length === 0)
@@ -84,6 +220,48 @@ async function hydrateFileTags(db, files) {
84
220
  }
85
221
  return files.map((f) => ({ ...f, tags: fileTagMap.get(f.id) || [] }));
86
222
  }
223
+ function buildMediaFileConditions(db, options) {
224
+ const conditions = [];
225
+ if (options.data) {
226
+ if (options.data.search) {
227
+ conditions.push(or(ilike(schema.mediaFilesTable.name, `%${options.data.search}%`), ilike(schema.mediaFilesTable.alt, `%${options.data.search}%`)));
228
+ }
229
+ if (options.data.ids) {
230
+ conditions.push(inArray(schema.mediaFilesTable.id, options.data.ids));
231
+ }
232
+ if (options.data.mimeTypes) {
233
+ const exact = options.data.mimeTypes.filter((t) => !t.endsWith('/*'));
234
+ const wildcards = options.data.mimeTypes.filter((t) => t.endsWith('/*'));
235
+ const mimeTypeConditions = wildcards.map((t) => ilike(schema.mediaFilesTable.mimeType, t.replace('*', '%')));
236
+ const typeConditions = wildcards.map((t) => {
237
+ const baseType = t.replace('/*', '');
238
+ return eq(schema.mediaFilesTable.type, baseType);
239
+ });
240
+ const mimeConditions = [
241
+ ...(exact.length ? [inArray(schema.mediaFilesTable.mimeType, exact)] : []),
242
+ ...mimeTypeConditions,
243
+ ...typeConditions
244
+ ];
245
+ if (mimeConditions.length) {
246
+ conditions.push(or(...mimeConditions));
247
+ }
248
+ }
249
+ if (options.data.tagIds && options.data.tagIds.length > 0) {
250
+ const fileIdsWithTags = db
251
+ .select({ fileId: schema.mediaFileTagsTable.fileId })
252
+ .from(schema.mediaFileTagsTable)
253
+ .where(inArray(schema.mediaFileTagsTable.tagId, options.data.tagIds));
254
+ conditions.push(inArray(schema.mediaFilesTable.id, fileIdsWithTags));
255
+ }
256
+ if (options.data.untagged) {
257
+ const fileIdsWithAnyTag = db
258
+ .select({ fileId: schema.mediaFileTagsTable.fileId })
259
+ .from(schema.mediaFileTagsTable);
260
+ conditions.push(sql `${schema.mediaFilesTable.id} NOT IN (${fileIdsWithAnyTag})`);
261
+ }
262
+ }
263
+ return conditions;
264
+ }
87
265
  export function pg(config) {
88
266
  const client = postgres(config.databaseUrl);
89
267
  const db = drizzle(client, { schema });
@@ -142,6 +320,92 @@ export function pg(config) {
142
320
  .where(and(...conditions));
143
321
  return result?.count ?? 0;
144
322
  },
323
+ getPaginatedEntries: async (options) => {
324
+ const versionWhereParts = buildCteVersionConditions(options);
325
+ const versionWhere = sql.join(versionWhereParts, sql ` AND `);
326
+ const entryWhereParts = buildCteEntryConditions(options);
327
+ const entryWhere = sql.join(entryWhereParts, sql ` AND `);
328
+ let orderClause;
329
+ if (options.dataOrderBy) {
330
+ const field = options.dataOrderBy.field;
331
+ if (!SAFE_JSON_KEY.test(field)) {
332
+ throw new Error(`Invalid dataOrderBy field: "${field}"`);
333
+ }
334
+ const dir = options.dataOrderBy.direction === 'desc' ? 'DESC' : 'ASC';
335
+ orderClause = sql.raw(`m.data->>'${field}' ${dir}`);
336
+ }
337
+ else if (options.orderBy) {
338
+ const col = options.orderBy.column === 'sortOrder' ? 'sort_order' : options.orderBy.column === 'createdAt' ? 'created_at' : 'updated_at';
339
+ orderClause = sql.raw(`e.${col} ${options.orderBy.direction}`);
340
+ }
341
+ else {
342
+ orderClause = sql.raw(`e.sort_order ASC`);
343
+ }
344
+ const rows = await db.execute(sql `
345
+ WITH matched AS (
346
+ SELECT DISTINCT ON (v.entry_id)
347
+ v.*
348
+ FROM entry_version v
349
+ WHERE ${versionWhere}
350
+ ORDER BY v.entry_id, v.version_number DESC
351
+ )
352
+ SELECT
353
+ e.id AS e_id, e.slug AS e_slug, e.type AS e_type,
354
+ e.created_at AS e_created_at, e.updated_at AS e_updated_at,
355
+ e.archived_at AS e_archived_at, e.sort_order AS e_sort_order,
356
+ m.id AS v_id, m.entry_id AS v_entry_id, m.lang AS v_lang,
357
+ m.version_number AS v_version_number, m.data AS v_data,
358
+ m.created_at AS v_created_at, m.created_by AS v_created_by,
359
+ m.published_at AS v_published_at, m.published_by AS v_published_by
360
+ FROM entry e
361
+ INNER JOIN matched m ON e.id = m.entry_id
362
+ WHERE ${entryWhere}
363
+ ORDER BY ${orderClause}
364
+ LIMIT ${options.limit} OFFSET ${options.offset}
365
+ `);
366
+ return rows.map((row) => ({
367
+ entry: {
368
+ id: row.e_id,
369
+ slug: row.e_slug,
370
+ type: row.e_type,
371
+ createdAt: new Date(row.e_created_at),
372
+ updatedAt: new Date(row.e_updated_at),
373
+ archivedAt: row.e_archived_at ? new Date(row.e_archived_at) : null,
374
+ sortOrder: row.e_sort_order
375
+ },
376
+ version: {
377
+ id: row.v_id,
378
+ entryId: row.v_entry_id,
379
+ lang: row.v_lang,
380
+ versionNumber: row.v_version_number,
381
+ data: row.v_data,
382
+ createdAt: new Date(row.v_created_at),
383
+ createdBy: row.v_created_by,
384
+ publishedAt: row.v_published_at ? new Date(row.v_published_at) : null,
385
+ publishedBy: row.v_published_by
386
+ }
387
+ }));
388
+ },
389
+ countPaginatedEntries: async (options) => {
390
+ const versionWhereParts = buildCteVersionConditions(options);
391
+ const versionWhere = sql.join(versionWhereParts, sql ` AND `);
392
+ const entryWhereParts = buildCteEntryConditions(options);
393
+ const entryWhere = sql.join(entryWhereParts, sql ` AND `);
394
+ const [result] = await db.execute(sql `
395
+ WITH matched AS (
396
+ SELECT DISTINCT ON (v.entry_id)
397
+ v.entry_id
398
+ FROM entry_version v
399
+ WHERE ${versionWhere}
400
+ ORDER BY v.entry_id, v.version_number DESC
401
+ )
402
+ SELECT COUNT(*)::int AS count
403
+ FROM entry e
404
+ INNER JOIN matched m ON e.id = m.entry_id
405
+ WHERE ${entryWhere}
406
+ `);
407
+ return result?.count ?? 0;
408
+ },
145
409
  updateEntry: async ({ id, data }) => {
146
410
  return db.transaction(async (tx) => {
147
411
  const [result] = await tx
@@ -182,7 +446,7 @@ export function pg(config) {
182
446
  .values({
183
447
  ...data,
184
448
  versionNumber: data.versionNumber ??
185
- sql `COALESCE((SELECT MAX(version_number) FROM ${schema.entryVersionsTable} WHERE entry_id = ${data.entryId}), 0) + 1`
449
+ sql `COALESCE((SELECT MAX(version_number) FROM ${schema.entryVersionsTable} WHERE entry_id = ${data.entryId} AND lang = ${data.lang}), 0) + 1`
186
450
  })
187
451
  .returning();
188
452
  return newVersion;
@@ -195,13 +459,17 @@ export function pg(config) {
195
459
  options.entryIds
196
460
  ? inArray(schema.entryVersionsTable.entryId, options.entryIds)
197
461
  : undefined,
198
- options.ids ? inArray(schema.entryVersionsTable.id, options.ids) : undefined
462
+ options.ids ? inArray(schema.entryVersionsTable.id, options.ids) : undefined,
463
+ options.lang ? eq(schema.entryVersionsTable.lang, options.lang) : undefined
199
464
  ].filter(Boolean);
200
465
  const jsonConditions = options.dataValues ? buildJsonConditions(options.dataValues) : [];
201
466
  const jsonLikeConditions = options.dataLike
202
467
  ? buildJsonLikeConditions(options.dataLike)
203
468
  : [];
204
- const allConditions = [...baseConditions, ...jsonConditions, ...jsonLikeConditions];
469
+ const jsonILikeOrCondition = options.dataILikeOr
470
+ ? buildJsonILikeOrConditions(options.dataILikeOr)
471
+ : undefined;
472
+ const allConditions = [...baseConditions, ...jsonConditions, ...jsonLikeConditions, ...(jsonILikeOrCondition ? [jsonILikeOrCondition] : [])];
205
473
  if (allConditions.length > 0) {
206
474
  query.where(and(...allConditions));
207
475
  }
@@ -351,48 +619,46 @@ export function pg(config) {
351
619
  await db.delete(schema.mediaFilesTable).where(inArray(schema.mediaFilesTable.id, ids));
352
620
  },
353
621
  getMediaFiles: async (options) => {
354
- const conditions = [];
355
- if (options.data) {
356
- if (options.data.search) {
357
- conditions.push(or(ilike(schema.mediaFilesTable.name, `%${options.data.search}%`), ilike(schema.mediaFilesTable.alt, `%${options.data.search}%`)));
358
- }
359
- if (options.data.ids) {
360
- conditions.push(inArray(schema.mediaFilesTable.id, options.data.ids));
361
- }
362
- if (options.data.mimeTypes) {
363
- const exact = options.data.mimeTypes.filter((t) => !t.endsWith('/*'));
364
- const wildcards = options.data.mimeTypes.filter((t) => t.endsWith('/*'));
365
- const mimeTypeConditions = wildcards.map((t) => ilike(schema.mediaFilesTable.mimeType, t.replace('*', '%')));
366
- // Fallback to type field for files with null mimeType
367
- const typeConditions = wildcards.map((t) => {
368
- const baseType = t.replace('/*', '');
369
- return eq(schema.mediaFilesTable.type, baseType);
370
- });
371
- const mimeConditions = [
372
- ...(exact.length ? [inArray(schema.mediaFilesTable.mimeType, exact)] : []),
373
- ...mimeTypeConditions,
374
- ...typeConditions
375
- ];
376
- if (mimeConditions.length) {
377
- conditions.push(or(...mimeConditions));
378
- }
379
- }
380
- if (options.data.tagIds && options.data.tagIds.length > 0) {
381
- const fileIdsWithTags = db
382
- .select({ fileId: schema.mediaFileTagsTable.fileId })
383
- .from(schema.mediaFileTagsTable)
384
- .where(inArray(schema.mediaFileTagsTable.tagId, options.data.tagIds));
385
- conditions.push(inArray(schema.mediaFilesTable.id, fileIdsWithTags));
386
- }
387
- }
622
+ const conditions = buildMediaFileConditions(db, options);
388
623
  const query = db
389
624
  .select()
390
625
  .from(schema.mediaFilesTable)
391
626
  .where(and(...conditions.filter(Boolean)))
392
627
  .orderBy(desc(schema.mediaFilesTable.createdAt));
628
+ if (options.data.limit != null) {
629
+ query.limit(options.data.limit);
630
+ }
631
+ if (options.data.offset != null) {
632
+ query.offset(options.data.offset);
633
+ }
393
634
  const rawFiles = await query;
394
635
  return hydrateFileTags(db, rawFiles);
395
636
  },
637
+ countMediaFiles: async (options) => {
638
+ const conditions = buildMediaFileConditions(db, options);
639
+ const [result] = await db
640
+ .select({ count: count() })
641
+ .from(schema.mediaFilesTable)
642
+ .where(and(...conditions.filter(Boolean)));
643
+ return result?.count ?? 0;
644
+ },
645
+ getMediaTagsWithCounts: async () => {
646
+ const rows = await db
647
+ .select({
648
+ id: schema.mediaTagsTable.id,
649
+ name: schema.mediaTagsTable.name,
650
+ color: schema.mediaTagsTable.color,
651
+ count: count(schema.mediaFileTagsTable.fileId)
652
+ })
653
+ .from(schema.mediaTagsTable)
654
+ .leftJoin(schema.mediaFileTagsTable, eq(schema.mediaTagsTable.id, schema.mediaFileTagsTable.tagId))
655
+ .groupBy(schema.mediaTagsTable.id, schema.mediaTagsTable.name, schema.mediaTagsTable.color)
656
+ .orderBy(schema.mediaTagsTable.name);
657
+ return rows.map((r) => ({
658
+ tag: { id: r.id, name: r.name, color: r.color },
659
+ count: r.count
660
+ }));
661
+ },
396
662
  getMediaFile: async (options) => {
397
663
  const query = db.select().from(schema.mediaFilesTable);
398
664
  if (options.data) {
@@ -55,49 +55,6 @@ export declare const entriesTable: import("drizzle-orm/pg-core/table", { with: {
55
55
  }, {}, {
56
56
  $type: "collection" | "singleton";
57
57
  }>;
58
- availableLocales: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
59
- name: "available_locales";
60
- tableName: "entry";
61
- dataType: "array";
62
- columnType: "PgArray";
63
- data: string[];
64
- driverParam: string | string[];
65
- notNull: false;
66
- hasDefault: false;
67
- isPrimaryKey: false;
68
- isAutoincrement: false;
69
- hasRuntimeDefault: false;
70
- enumValues: [string, ...string[]];
71
- baseColumn: import("drizzle-orm/column", { with: { "resolution-mode": "require" } }).Column<{
72
- name: "available_locales";
73
- tableName: "entry";
74
- dataType: "string";
75
- columnType: "PgText";
76
- data: string;
77
- driverParam: string;
78
- notNull: false;
79
- hasDefault: false;
80
- isPrimaryKey: false;
81
- isAutoincrement: false;
82
- hasRuntimeDefault: false;
83
- enumValues: [string, ...string[]];
84
- baseColumn: never;
85
- identity: undefined;
86
- generated: undefined;
87
- }, {}, {}>;
88
- identity: undefined;
89
- generated: undefined;
90
- }, {}, {
91
- size: undefined;
92
- baseBuilder: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumnBuilder<{
93
- name: "available_locales";
94
- dataType: "string";
95
- columnType: "PgText";
96
- data: string;
97
- enumValues: [string, ...string[]];
98
- driverParam: string;
99
- }, {}, {}, import("drizzle-orm/column-builder", { with: { "resolution-mode": "require" } }).ColumnBuilderExtraConfig>;
100
- }>;
101
58
  createdAt: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
102
59
  name: "created_at";
103
60
  tableName: "entry";
@@ -149,57 +106,6 @@ export declare const entriesTable: import("drizzle-orm/pg-core/table", { with: {
149
106
  identity: undefined;
150
107
  generated: undefined;
151
108
  }, {}, {}>;
152
- publishedAt: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
153
- name: "published_at";
154
- tableName: "entry";
155
- dataType: "date";
156
- columnType: "PgTimestamp";
157
- data: Date;
158
- driverParam: string;
159
- notNull: false;
160
- hasDefault: false;
161
- isPrimaryKey: false;
162
- isAutoincrement: false;
163
- hasRuntimeDefault: false;
164
- enumValues: undefined;
165
- baseColumn: never;
166
- identity: undefined;
167
- generated: undefined;
168
- }, {}, {}>;
169
- publishedVersionId: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
170
- name: "published_version_id";
171
- tableName: "entry";
172
- dataType: "string";
173
- columnType: "PgUUID";
174
- data: string;
175
- driverParam: string;
176
- notNull: false;
177
- hasDefault: false;
178
- isPrimaryKey: false;
179
- isAutoincrement: false;
180
- hasRuntimeDefault: false;
181
- enumValues: undefined;
182
- baseColumn: never;
183
- identity: undefined;
184
- generated: undefined;
185
- }, {}, {}>;
186
- publishedBy: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
187
- name: "published_by";
188
- tableName: "entry";
189
- dataType: "string";
190
- columnType: "PgText";
191
- data: string;
192
- driverParam: string;
193
- notNull: false;
194
- hasDefault: false;
195
- isPrimaryKey: false;
196
- isAutoincrement: false;
197
- hasRuntimeDefault: false;
198
- enumValues: [string, ...string[]];
199
- baseColumn: never;
200
- identity: undefined;
201
- generated: undefined;
202
- }, {}, {}>;
203
109
  sortOrder: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
204
110
  name: "sort_order";
205
111
  tableName: "entry";
@@ -1,18 +1,12 @@
1
1
  import { integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
2
- import { entryVersionsTable } from './entryVersion.js';
3
2
  export const entriesTable = pgTable('entry', {
4
3
  id: uuid('id').primaryKey().defaultRandom(),
5
4
  slug: text('slug').notNull(),
6
5
  type: text('type').notNull().$type(),
7
- availableLocales: text('available_locales').array(),
8
6
  // Metadane
9
7
  createdAt: timestamp('created_at').defaultNow().notNull(),
10
8
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
11
9
  archivedAt: timestamp('archived_at'), // soft delete / archiwum
12
- // Publish state
13
- publishedAt: timestamp('published_at'),
14
- publishedVersionId: uuid('published_version_id').references(() => entryVersionsTable.id, { onDelete: 'set null' }),
15
- publishedBy: text('published_by'),
16
10
  // Manual ordering
17
11
  sortOrder: integer('sort_order')
18
12
  });
@@ -37,6 +37,23 @@ export declare const entryVersionsTable: import("drizzle-orm/pg-core/table", { w
37
37
  identity: undefined;
38
38
  generated: undefined;
39
39
  }, {}, {}>;
40
+ lang: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
41
+ name: "lang";
42
+ tableName: "entry_version";
43
+ dataType: "string";
44
+ columnType: "PgText";
45
+ data: string;
46
+ driverParam: string;
47
+ notNull: true;
48
+ hasDefault: false;
49
+ isPrimaryKey: false;
50
+ isAutoincrement: false;
51
+ hasRuntimeDefault: false;
52
+ enumValues: [string, ...string[]];
53
+ baseColumn: never;
54
+ identity: undefined;
55
+ generated: undefined;
56
+ }, {}, {}>;
40
57
  versionNumber: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
41
58
  name: "version_number";
42
59
  tableName: "entry_version";