includio-cms 0.0.60 → 0.0.62

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.
@@ -29,6 +29,7 @@
29
29
  import { createHybridContext } from './hybrid/hybrid-context.svelte.js';
30
30
  import HybridLayout from './hybrid/hybrid-layout.svelte';
31
31
  import { onMount } from 'svelte';
32
+ import { get } from 'svelte/store';
32
33
 
33
34
  const contentLanguage = getContentLanguage();
34
35
  const remotes = getRemotes();
@@ -130,10 +131,8 @@
130
131
  }
131
132
 
132
133
  async function performAutosave() {
133
- const validatedForm = await form.validateForm();
134
- if (!validatedForm.valid) return;
135
-
136
- const currentData = JSON.stringify(validatedForm.data);
134
+ const currentFormData = get(form.form);
135
+ const currentData = JSON.stringify(currentFormData);
137
136
  if (currentData === lastSavedData) {
138
137
  saveStatus = 'saved';
139
138
  return;
@@ -143,7 +142,7 @@
143
142
  try {
144
143
  await remotes.updateEntryVersionCommand({
145
144
  entryId: entry.id,
146
- data: validatedForm.data,
145
+ data: currentFormData,
147
146
  type: 'draft'
148
147
  });
149
148
  lastSavedData = currentData;
@@ -185,6 +184,31 @@
185
184
  autosaveTimer = null;
186
185
  }
187
186
 
187
+ if (type === 'draft') {
188
+ // Drafts save without validation
189
+ const currentFormData = get(form.form);
190
+ saveStatus = 'saving';
191
+ try {
192
+ await remotes.updateEntryVersionCommand({
193
+ entryId: entry.id,
194
+ data: currentFormData,
195
+ type: 'draft'
196
+ });
197
+ lastSavedData = JSON.stringify(currentFormData);
198
+ saveStatus = 'saved';
199
+ toast.success(lang[interfaceLanguage.current].saveDraftToast);
200
+ previewToggle = !previewToggle;
201
+ setTimeout(() => {
202
+ if (saveStatus === 'saved') saveStatus = 'idle';
203
+ }, 3000);
204
+ } catch {
205
+ saveStatus = 'error';
206
+ toast.error(interfaceLanguage.current === 'pl' ? 'Błąd zapisu' : 'Save failed');
207
+ }
208
+ return;
209
+ }
210
+
211
+ // Publish requires validation
188
212
  const validatedForm = await form.validateForm();
189
213
 
190
214
  if (validatedForm.valid) {
@@ -203,9 +227,7 @@
203
227
  const toastMsg =
204
228
  type === 'published-now'
205
229
  ? lang[interfaceLanguage.current].publishToast
206
- : type === 'published-scheduled'
207
- ? lang[interfaceLanguage.current].scheduledToast
208
- : lang[interfaceLanguage.current].saveToast;
230
+ : lang[interfaceLanguage.current].scheduledToast;
209
231
  toast.success(toastMsg);
210
232
  previewToggle = !previewToggle;
211
233
 
@@ -220,8 +242,8 @@
220
242
  const errors = flattenErrors(validatedForm.errors, collection.fields);
221
243
  toast.error(
222
244
  interfaceLanguage.current === 'pl'
223
- ? 'Nie można zapisać'
224
- : 'Cannot save',
245
+ ? 'Nie można opublikować'
246
+ : 'Cannot publish',
225
247
  {
226
248
  description: errors.slice(0, 3).join('\n') + (errors.length > 3 ? '\n...' : ''),
227
249
  duration: 5000
@@ -82,7 +82,7 @@ export const updateEntryVersionCommand = command(updateEntryVersionCommandSchema
82
82
  let result;
83
83
  switch (input.type) {
84
84
  case 'draft':
85
- result = await createEntryVersion(input);
85
+ result = await createEntryVersion(input, { skipValidation: true });
86
86
  break;
87
87
  case 'published-now':
88
88
  result = await createEntryVersion({
@@ -8,4 +8,6 @@ export declare const createEntrySchema: z.ZodObject<{
8
8
  }>;
9
9
  }, z.z.core.$strip>;
10
10
  export declare const createEntry: (data: DbEntryInsert) => Promise<DbEntry>;
11
- export declare const createEntryVersion: (data: DbEntryVersionInsert) => Promise<DbEntryVersion>;
11
+ export declare const createEntryVersion: (data: DbEntryVersionInsert, options?: {
12
+ skipValidation?: boolean;
13
+ }) => Promise<DbEntryVersion>;
@@ -22,20 +22,27 @@ export const createEntry = async (data) => {
22
22
  });
23
23
  return newDbEntry;
24
24
  };
25
- export const createEntryVersion = async (data) => {
25
+ export const createEntryVersion = async (data, options) => {
26
26
  const { user } = requireAuth();
27
- const entry = await getDbEntryOrThrow({ id: data.entryId });
28
- const config = getCMS().getBySlug(entry.slug);
29
- const languages = getCMS().languages;
30
- const schema = generateZodSchemaFromFields(config.fields, languages);
31
- const parsedData = schema.safeParse(data.data);
32
- if (!parsedData.success) {
33
- throw Error('Invalid data: ' + JSON.stringify(parsedData.error.flatten()));
27
+ let validatedData;
28
+ if (options?.skipValidation) {
29
+ validatedData = data.data;
30
+ }
31
+ else {
32
+ const entry = await getDbEntryOrThrow({ id: data.entryId });
33
+ const config = getCMS().getBySlug(entry.slug);
34
+ const languages = getCMS().languages;
35
+ const schema = generateZodSchemaFromFields(config.fields, languages);
36
+ const parsedData = schema.safeParse(data.data);
37
+ if (!parsedData.success) {
38
+ throw Error('Invalid data: ' + JSON.stringify(parsedData.error.flatten()));
39
+ }
40
+ validatedData = parsedData.data;
34
41
  }
35
42
  const newEntryVersion = await getCMS().databaseAdapter.createEntryVersion({
36
43
  ...data,
37
44
  createdBy: user.id,
38
- data: parsedData.data
45
+ data: validatedData
39
46
  });
40
47
  return newEntryVersion;
41
48
  };
@@ -89,6 +89,47 @@ export const getRawEntryOrThrow = async (options) => {
89
89
  }
90
90
  return entry;
91
91
  };
92
+ function getNestedValue(obj, keys) {
93
+ let current = obj;
94
+ for (const key of keys) {
95
+ if (current === null || current === undefined || typeof current !== 'object')
96
+ return undefined;
97
+ current = current[key];
98
+ }
99
+ return current;
100
+ }
101
+ function matchesDataValues(data, filter, prefix = []) {
102
+ for (const [key, value] of Object.entries(filter)) {
103
+ const path = [...prefix, key];
104
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
105
+ if (!matchesDataValues(data, value, path))
106
+ return false;
107
+ }
108
+ else {
109
+ const actual = getNestedValue(data, path);
110
+ if (actual !== value)
111
+ return false;
112
+ }
113
+ }
114
+ return true;
115
+ }
116
+ function matchesDataLike(data, filter, prefix = []) {
117
+ for (const [key, value] of Object.entries(filter)) {
118
+ const path = [...prefix, key];
119
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
120
+ if (!matchesDataLike(data, value, path))
121
+ return false;
122
+ }
123
+ else if (typeof value === 'string') {
124
+ const actual = getNestedValue(data, path);
125
+ if (typeof actual !== 'string')
126
+ return false;
127
+ if (!actual.toLowerCase().includes(value.toLowerCase()))
128
+ return false;
129
+ }
130
+ }
131
+ return true;
132
+ }
92
133
  export const getEntries = async (options = {}) => {
93
134
  const language = options.language || getCMS().languages[0];
94
135
  const status = options.status || 'published';
@@ -111,11 +152,9 @@ export const getEntries = async (options = {}) => {
111
152
  const entryIds = dbEntries.map((entry) => entry.id);
112
153
  // Build entries map for quick lookup
113
154
  const entriesMap = new Map(dbEntries.map((entry) => [entry.id, entry]));
114
- // Get versions and keep only the latest per entry in a single pass
155
+ // Get versions WITHOUT dataValues/dataLike - filter after finding latest
115
156
  const dbEntryVersions = await getCMS().databaseAdapter.getEntryVersions({
116
157
  entryIds,
117
- dataValues,
118
- dataLike,
119
158
  status
120
159
  });
121
160
  // Group versions by entryId, keeping only the latest version for each entry
@@ -136,6 +175,21 @@ export const getEntries = async (options = {}) => {
136
175
  }
137
176
  }
138
177
  }
178
+ // Post-filter: apply dataValues/dataLike on latest versions only
179
+ if (dataValues) {
180
+ for (const [entryId, entry] of latestVersionsByEntry) {
181
+ if (!matchesDataValues(entry.version.data, dataValues)) {
182
+ latestVersionsByEntry.delete(entryId);
183
+ }
184
+ }
185
+ }
186
+ if (dataLike) {
187
+ for (const [entryId, entry] of latestVersionsByEntry) {
188
+ if (!matchesDataLike(entry.version.data, dataLike)) {
189
+ latestVersionsByEntry.delete(entryId);
190
+ }
191
+ }
192
+ }
139
193
  // Process entries in parallel
140
194
  const entries = await Promise.all(Array.from(latestVersionsByEntry.values()).map(async ({ version, dbEntry }) => {
141
195
  try {
@@ -4,7 +4,7 @@ import { resolveRelationFields } from './resolveRelationFields.js';
4
4
  import { resolveUrlFields } from './resolveUrlFields.js';
5
5
  export async function populateEntryData(data, fields, language) {
6
6
  let populatedData = await resolveRelationFields(data, fields, language);
7
- populatedData = await resolveUrlFields(populatedData, fields);
7
+ populatedData = await resolveUrlFields(populatedData, fields, language);
8
8
  populatedData = await resolveMediaFields(populatedData, fields);
9
9
  populatedData = translateObject(populatedData, language);
10
10
  return populatedData;
@@ -1,3 +1,3 @@
1
1
  import type { EntryData, PopulatedEntryData } from '../../../types/entries.js';
2
2
  import type { Field } from '../../../types/fields.js';
3
- export declare function resolveUrlFields(data: EntryData, fields: Field[]): Promise<PopulatedEntryData>;
3
+ export declare function resolveUrlFields(data: EntryData, fields: Field[], language: string): Promise<PopulatedEntryData>;
@@ -1,6 +1,6 @@
1
1
  import { urlFieldDataSchema, urlFieldDataWithRelationSchema } from '../../../schemas/field/url.js';
2
2
  import { getEntries } from '../entries/operations/get.js';
3
- export async function resolveUrlFields(data, fields) {
3
+ export async function resolveUrlFields(data, fields, language) {
4
4
  const entriesIds = [];
5
5
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
6
  const collectIds = (value, fields) => {
@@ -36,7 +36,7 @@ export async function resolveUrlFields(data, fields) {
36
36
  collectIds(data, fields);
37
37
  if (entriesIds.length === 0)
38
38
  return data;
39
- const entries = await getEntries({ ids: entriesIds });
39
+ const entries = await getEntries({ ids: entriesIds, language });
40
40
  const entriesMap = Object.fromEntries(entries.map((m) => [m.id, m]));
41
41
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
42
  const resolveValues = (value, fields) => {
@@ -293,7 +293,17 @@ export function pg(config) {
293
293
  query.where(and(options.data.folderId
294
294
  ? eq(schema.mediaFilesTable.folderId, options.data.folderId)
295
295
  : undefined, options.data.ids ? inArray(schema.mediaFilesTable.id, options.data.ids) : undefined, options.data.mimeTypes
296
- ? inArray(schema.mediaFilesTable.mimeType, options.data.mimeTypes)
296
+ ? (() => {
297
+ const exact = options.data.mimeTypes.filter((t) => !t.endsWith('/*'));
298
+ const wildcards = options.data.mimeTypes
299
+ .filter((t) => t.endsWith('/*'))
300
+ .map((t) => ilike(schema.mediaFilesTable.mimeType, t.replace('*', '%')));
301
+ const conditions = [
302
+ ...(exact.length ? [inArray(schema.mediaFilesTable.mimeType, exact)] : []),
303
+ ...wildcards
304
+ ];
305
+ return conditions.length ? or(...conditions) : undefined;
306
+ })()
297
307
  : undefined, searchCondition));
298
308
  }
299
309
  return await query.orderBy(desc(schema.mediaFilesTable.createdAt));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "includio-cms",
3
- "version": "0.0.60",
3
+ "version": "0.0.62",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",