includio-cms 0.6.2 → 0.7.1

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 (78) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/ROADMAP.md +18 -2
  3. package/dist/admin/api/accept-invite.js +2 -2
  4. package/dist/admin/auth-client.d.ts +2122 -2122
  5. package/dist/admin/client/admin/admin-layout.svelte +5 -1
  6. package/dist/admin/client/admin/admin-layout.svelte.d.ts +2 -0
  7. package/dist/admin/client/index.d.ts +1 -0
  8. package/dist/admin/client/index.js +1 -0
  9. package/dist/admin/components/fields/blocks-field.svelte +3 -3
  10. package/dist/admin/components/fields/field-renderer.svelte +9 -0
  11. package/dist/admin/components/fields/image-field.svelte +2 -2
  12. package/dist/admin/components/fields/media-field.svelte +4 -4
  13. package/dist/admin/components/media/file/file-miniature.svelte +6 -6
  14. package/dist/admin/helpers/build-custom-fields-map.d.ts +7 -0
  15. package/dist/admin/helpers/build-custom-fields-map.js +13 -0
  16. package/dist/admin/helpers/index.d.ts +7 -0
  17. package/dist/admin/helpers/index.js +7 -0
  18. package/dist/admin/helpers/use-field.d.ts +26 -0
  19. package/dist/admin/helpers/use-field.js +9 -0
  20. package/dist/admin/state/custom-fields.svelte.d.ts +3 -0
  21. package/dist/admin/state/custom-fields.svelte.js +10 -0
  22. package/dist/admin/styles/admin.css +1 -1
  23. package/dist/admin/ui/index.d.ts +12 -0
  24. package/dist/admin/ui/index.js +14 -0
  25. package/dist/components/ui/accordion/accordion.svelte.d.ts +1 -1
  26. package/dist/components/ui/calendar/calendar.svelte.d.ts +1 -1
  27. package/dist/components/ui/command/command-dialog.svelte.d.ts +1 -1
  28. package/dist/components/ui/command/command-input.svelte.d.ts +1 -1
  29. package/dist/components/ui/command/command.svelte.d.ts +1 -1
  30. package/dist/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte.d.ts +1 -1
  31. package/dist/components/ui/input/input.svelte +2 -2
  32. package/dist/components/ui/input/input.svelte.d.ts +1 -1
  33. package/dist/components/ui/input-group/input-group-input.svelte.d.ts +2 -2
  34. package/dist/components/ui/input-group/input-group-textarea.svelte.d.ts +1 -1
  35. package/dist/components/ui/input-group/input-group.svelte +1 -1
  36. package/dist/components/ui/radio-group/radio-group.svelte.d.ts +1 -1
  37. package/dist/components/ui/select/select-trigger.svelte +1 -1
  38. package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +2 -2
  39. package/dist/components/ui/tabs/tabs.svelte.d.ts +1 -1
  40. package/dist/components/ui/textarea/textarea.svelte +1 -1
  41. package/dist/components/ui/textarea/textarea.svelte.d.ts +1 -1
  42. package/dist/components/ui/toggle-group/toggle-group-item.svelte.d.ts +1 -1
  43. package/dist/components/ui/toggle-group/toggle-group.svelte.d.ts +1 -1
  44. package/dist/core/cms.d.ts +7 -4
  45. package/dist/core/cms.js +52 -2
  46. package/dist/core/fields/fieldSchemaToTs.d.ts +11 -0
  47. package/dist/core/fields/fieldSchemaToTs.js +30 -0
  48. package/dist/core/index.d.ts +1 -0
  49. package/dist/core/index.js +1 -0
  50. package/dist/core/server/fields/populateEntry.js +50 -0
  51. package/dist/core/server/fields/resolveRichtextLinks.js +3 -2
  52. package/dist/core/server/fields/resolveUrlFields.js +2 -2
  53. package/dist/core/server/fields/slugResolver.d.ts +5 -0
  54. package/dist/core/server/fields/slugResolver.js +16 -0
  55. package/dist/core/server/fields/utils/resolveMedia.d.ts +12 -0
  56. package/dist/core/server/fields/utils/resolveMedia.js +23 -0
  57. package/dist/core/server/generator/fields.d.ts +2 -0
  58. package/dist/core/server/generator/fields.js +8 -0
  59. package/dist/core/server/generator/generator.js +9 -1
  60. package/dist/db-postgres/index.d.ts +6 -1
  61. package/dist/db-postgres/index.js +1 -0
  62. package/dist/server/auth.d.ts +1 -1358
  63. package/dist/server/auth.js +3 -23
  64. package/dist/sveltekit/index.d.ts +1 -0
  65. package/dist/sveltekit/index.js +1 -0
  66. package/dist/sveltekit/server/handle.js +21 -2
  67. package/dist/types/cms.d.ts +8 -3
  68. package/dist/types/config.d.ts +1 -0
  69. package/dist/types/fields.d.ts +9 -2
  70. package/dist/types/index.d.ts +2 -1
  71. package/dist/types/index.js +1 -0
  72. package/dist/types/plugins.d.ts +19 -1
  73. package/dist/updates/0.7.0/index.d.ts +2 -0
  74. package/dist/updates/0.7.0/index.js +16 -0
  75. package/dist/updates/0.7.1/index.d.ts +2 -0
  76. package/dist/updates/0.7.1/index.js +16 -0
  77. package/dist/updates/index.js +3 -1
  78. package/package.json +14 -1
package/dist/core/cms.js CHANGED
@@ -1,23 +1,31 @@
1
+ import { setSchemaGetCMS } from './fields/fieldSchemaToTs.js';
2
+ import { betterAuth } from 'better-auth';
3
+ import { drizzleAdapter } from 'better-auth/adapters/drizzle';
4
+ import { admin } from 'better-auth/plugins';
5
+ import { resetPasswordEmailTemplate } from '../admin/email/reset-password-template.js';
6
+ import * as authSchema from '../server/db/schema/auth-schema.js';
1
7
  export class CMS {
2
8
  config;
3
9
  databaseAdapter;
4
10
  filesAdapter;
5
11
  emailAdapter;
6
- auth;
12
+ authConfig;
7
13
  aiAdapter = null;
14
+ _betterAuth = null;
8
15
  collections;
9
16
  singles;
10
17
  forms;
11
18
  languages;
12
19
  mediaConfig;
13
20
  plugins = [];
21
+ customFields = new Map();
14
22
  apiKeys = [];
15
23
  constructor(config) {
16
24
  this.config = config;
17
25
  this.databaseAdapter = config.db;
18
26
  this.filesAdapter = config.files;
19
27
  this.emailAdapter = config.email ?? null;
20
- this.auth = config.auth ?? null;
28
+ this.authConfig = config.auth ?? null;
21
29
  this.aiAdapter = config.ai || null;
22
30
  this.mediaConfig = config.media || {};
23
31
  this.collections = {};
@@ -44,7 +52,49 @@ export class CMS {
44
52
  this.apiKeys = config.apiKeys || [];
45
53
  if (config.plugins) {
46
54
  this.plugins = config.plugins;
55
+ for (const plugin of this.plugins) {
56
+ for (const def of plugin.fields ?? []) {
57
+ if (this.customFields.has(def.fieldType)) {
58
+ throw new Error(`Duplicate custom field type: "${def.fieldType}" (plugin: "${plugin.slug}")`);
59
+ }
60
+ this.customFields.set(def.fieldType, def);
61
+ }
62
+ }
47
63
  }
64
+ setSchemaGetCMS(() => this);
65
+ }
66
+ get auth() {
67
+ if (!this._betterAuth) {
68
+ if (!this.authConfig) {
69
+ throw new Error('Auth not configured. Add `auth` to your CMS config.');
70
+ }
71
+ const drizzleDb = this.databaseAdapter._drizzle;
72
+ if (!drizzleDb) {
73
+ throw new Error('Database adapter does not expose a drizzle instance (_drizzle).');
74
+ }
75
+ const emailAdapter = this.emailAdapter;
76
+ this._betterAuth = betterAuth({
77
+ database: drizzleAdapter(drizzleDb, { provider: 'pg', schema: authSchema }),
78
+ secret: this.authConfig.secret,
79
+ baseURL: this.authConfig.baseURL,
80
+ emailAndPassword: {
81
+ enabled: true,
82
+ async sendResetPassword({ user, url }, request) {
83
+ if (!emailAdapter) {
84
+ throw new Error('Email adapter not configured');
85
+ }
86
+ const lang = request?.headers.get('Accept-Language')?.startsWith('pl') ? 'pl' : 'en';
87
+ const { subject, html } = resetPasswordEmailTemplate({
88
+ resetUrl: url,
89
+ lang
90
+ });
91
+ await emailAdapter.sendMail({ to: [user.email], subject, html });
92
+ }
93
+ },
94
+ plugins: [admin()]
95
+ });
96
+ }
97
+ return this._betterAuth;
48
98
  }
49
99
  getBySlug(slug) {
50
100
  const config = this.collections[slug] || this.singles[slug];
@@ -1,5 +1,16 @@
1
1
  import type { Field } from '../../types/fields.js';
2
+ import type { CustomFieldDefinition } from '../../types/plugins.js';
2
3
  import { z } from 'zod';
4
+ /**
5
+ * Set custom fields map for client-side use (where getCMS() is unavailable).
6
+ */
7
+ export declare function setSchemaCustomFields(customFields: Map<string, CustomFieldDefinition>): void;
8
+ /**
9
+ * Set CMS getter for server-side use (avoids static import of server-only cms.ts).
10
+ */
11
+ export declare function setSchemaGetCMS(getter: () => {
12
+ customFields: Map<string, CustomFieldDefinition>;
13
+ }): void;
3
14
  type AnyZodObject = z.ZodObject<any, any>;
4
15
  interface GenerateZodSchemaOptions {
5
16
  parentRequired?: boolean;
@@ -1,4 +1,28 @@
1
1
  import { z } from 'zod';
2
+ let _customFieldsOverride = null;
3
+ /**
4
+ * Set custom fields map for client-side use (where getCMS() is unavailable).
5
+ */
6
+ export function setSchemaCustomFields(customFields) {
7
+ _customFieldsOverride = customFields;
8
+ }
9
+ let _getCMS = null;
10
+ /**
11
+ * Set CMS getter for server-side use (avoids static import of server-only cms.ts).
12
+ */
13
+ export function setSchemaGetCMS(getter) {
14
+ _getCMS = getter;
15
+ }
16
+ function getCustomFieldDef(fieldType) {
17
+ if (_customFieldsOverride)
18
+ return _customFieldsOverride.get(fieldType);
19
+ try {
20
+ return _getCMS?.().customFields.get(fieldType);
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ }
2
26
  export function generateZodSchemaFromField(field, languages, options = {
3
27
  parentRequired: true
4
28
  }) {
@@ -283,6 +307,12 @@ export function generateZodSchemaFromField(field, languages, options = {
283
307
  }
284
308
  return schema;
285
309
  }
310
+ case 'custom': {
311
+ const customDef = getCustomFieldDef(field.fieldType);
312
+ if (!customDef)
313
+ return z.any();
314
+ return customDef.zodSchema(field, languages);
315
+ }
286
316
  default:
287
317
  return z.any();
288
318
  }
@@ -1 +1,2 @@
1
1
  export { getCMS } from './cms.js';
2
+ export { resolveMediaWithStyles, type ResolvedMedia } from './server/fields/utils/resolveMedia.js';
@@ -1 +1,2 @@
1
1
  export { getCMS } from './cms.js';
2
+ export { resolveMediaWithStyles } from './server/fields/utils/resolveMedia.js';
@@ -4,6 +4,7 @@ import { resolveMediaFields } from './resolveImageFields.js';
4
4
  import { resolveRelationFields } from './resolveRelationFields.js';
5
5
  import { resolveRichtextLinks } from './resolveRichtextLinks.js';
6
6
  import { resolveUrlFields } from './resolveUrlFields.js';
7
+ import { getCMS } from '../../cms.js';
7
8
  function translateInlineBlockData(data, fields, language) {
8
9
  for (const field of fields) {
9
10
  const val = data[field.slug];
@@ -45,11 +46,60 @@ function translateInlineBlockData(data, fields, language) {
45
46
  }
46
47
  }
47
48
  }
49
+ async function resolveCustomFields(data, fields) {
50
+ // Check if any custom fields exist before accessing CMS
51
+ const hasCustom = fields.some((f) => f.type === 'custom' ||
52
+ f.type === 'object' ||
53
+ f.type === 'blocks');
54
+ if (!hasCustom)
55
+ return data;
56
+ let cms;
57
+ try {
58
+ cms = getCMS();
59
+ }
60
+ catch {
61
+ return data;
62
+ }
63
+ const result = { ...data };
64
+ for (const field of fields) {
65
+ const val = data[field.slug];
66
+ switch (field.type) {
67
+ case 'custom': {
68
+ const def = cms.customFields.get(field.fieldType);
69
+ if (def?.populateResolver && val != null) {
70
+ result[field.slug] = await def.populateResolver(val, field);
71
+ }
72
+ break;
73
+ }
74
+ case 'object':
75
+ if (val && typeof val === 'object' && 'data' in val) {
76
+ result[field.slug] = {
77
+ ...val,
78
+ data: await resolveCustomFields(val.data, field.fields)
79
+ };
80
+ }
81
+ break;
82
+ case 'blocks':
83
+ if (Array.isArray(val)) {
84
+ result[field.slug] = await Promise.all(val.map(async (item) => {
85
+ const blockDef = field.of.find((d) => d.slug === item.slug);
86
+ if (blockDef) {
87
+ return { ...item, data: await resolveCustomFields(item.data, blockDef.fields) };
88
+ }
89
+ return item;
90
+ }));
91
+ }
92
+ break;
93
+ }
94
+ }
95
+ return result;
96
+ }
48
97
  export async function populateEntryData(data, fields, language) {
49
98
  let populatedData = await resolveRelationFields(data, fields, language);
50
99
  populatedData = await resolveUrlFields(populatedData, fields, language);
51
100
  populatedData = await resolveMediaFields(populatedData, fields);
52
101
  populatedData = await resolveRichtextLinks(populatedData, fields, language);
102
+ populatedData = (await resolveCustomFields(populatedData, fields));
53
103
  populatedData = translateObject(populatedData, language);
54
104
  translateInlineBlockData(populatedData, fields, language);
55
105
  return populatedData;
@@ -1,6 +1,6 @@
1
1
  import { getCMS } from '../../cms.js';
2
2
  import { extractEntryIds as extractEntryIdsFromDoc, extractMediaIds as extractMediaIdsFromDoc, cloneDoc, walkLinkMarks, walkInlineBlockNodes } from '../../../admin/components/tiptap/structured-content-utils.js';
3
- import { getEntrySlugPath, getSlugFromEntryData } from './slugResolver.js';
3
+ import { getEntrySlugPath, getSlugFromEntryData, getEntryPath } from './slugResolver.js';
4
4
  const ENTRY_ID_RE = /data-entry-id="([0-9a-f-]{36})"/g;
5
5
  function extractEntryIds(html) {
6
6
  const ids = [];
@@ -130,7 +130,8 @@ export async function resolveRichtextLinks(data, fields, language) {
130
130
  const slugPath = configSlug ? getEntrySlugPath(configSlug) : 'seo.slug';
131
131
  const slug = getSlugFromEntryData(rawData, slugPath, language);
132
132
  if (slug) {
133
- slugMap[version.entryId] = slug;
133
+ const configSlug = entryConfigSlugMap[version.entryId];
134
+ slugMap[version.entryId] = configSlug ? getEntryPath(configSlug, slug) : slug;
134
135
  }
135
136
  }
136
137
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -1,7 +1,7 @@
1
1
  import { urlFieldDataSchema, urlFieldDataWithRelationSchema } from '../../../schemas/field/url.js';
2
2
  import { walkInlineBlockNodes, cloneDoc } from '../../../admin/components/tiptap/structured-content-utils.js';
3
3
  import { getDbEntries, getDbEntryVersions } from '../entries/operations/get.js';
4
- import { getEntrySlugPath, getSlugFromEntryData } from './slugResolver.js';
4
+ import { getEntrySlugPath, getSlugFromEntryData, getEntryPath } from './slugResolver.js';
5
5
  import { isExternalUrl, mergeRel } from '../../fields/urlUtils.js';
6
6
  const FLAT_KEY = '_flat';
7
7
  /**
@@ -121,7 +121,7 @@ export async function resolveUrlFields(data, fields, language) {
121
121
  const slugPath = getEntrySlugPath(entry.slug);
122
122
  const slug = getSlugFromEntryData(version.data, slugPath, language);
123
123
  if (slug)
124
- slugMap[entry.id] = slug;
124
+ slugMap[entry.id] = getEntryPath(entry.slug, slug);
125
125
  }
126
126
  }
127
127
  }
@@ -8,3 +8,8 @@ export declare function getEntrySlugPath(configSlug: string): string;
8
8
  * Handles both plain strings and localized objects { lang: string }.
9
9
  */
10
10
  export declare function getSlugFromEntryData(data: Record<string, unknown>, slugPath: string, language?: string): string | undefined;
11
+ /**
12
+ * Build full entry path using collection's pathTemplate.
13
+ * If no template configured, returns the raw slug.
14
+ */
15
+ export declare function getEntryPath(configSlug: string, slug: string): string;
@@ -32,3 +32,19 @@ export function getSlugFromEntryData(data, slugPath, language) {
32
32
  }
33
33
  return undefined;
34
34
  }
35
+ /**
36
+ * Build full entry path using collection's pathTemplate.
37
+ * If no template configured, returns the raw slug.
38
+ */
39
+ export function getEntryPath(configSlug, slug) {
40
+ try {
41
+ const config = getCMS().getBySlug(configSlug);
42
+ if (config.pathTemplate) {
43
+ return config.pathTemplate.replace('{slug}', slug);
44
+ }
45
+ }
46
+ catch {
47
+ // Config not found — fall through
48
+ }
49
+ return slug;
50
+ }
@@ -0,0 +1,12 @@
1
+ import type { ImageFieldStyle } from '../../../../types/fields.js';
2
+ import type { ImageStyle, MediaFile } from '../../../../types/media.js';
3
+ export interface ResolvedMedia {
4
+ data: MediaFile;
5
+ styles: Record<string, ImageStyle>;
6
+ blurDataUrl: string | null;
7
+ }
8
+ /**
9
+ * Resolve media files by IDs and generate image styles.
10
+ * Useful for plugins that need to resolve media references (e.g. photo-grid).
11
+ */
12
+ export declare function resolveMediaWithStyles(mediaIds: string[], styles?: ImageFieldStyle[]): Promise<Record<string, ResolvedMedia>>;
@@ -0,0 +1,23 @@
1
+ import { getCMS } from '../../../cms.js';
2
+ import { getImageStyles } from './imageStyles.js';
3
+ /**
4
+ * Resolve media files by IDs and generate image styles.
5
+ * Useful for plugins that need to resolve media references (e.g. photo-grid).
6
+ */
7
+ export async function resolveMediaWithStyles(mediaIds, styles) {
8
+ if (!mediaIds.length)
9
+ return {};
10
+ const cms = getCMS();
11
+ const files = await cms.databaseAdapter.getMediaFiles({ data: { ids: mediaIds } });
12
+ // Synthetic ImageField to pass to getImageStyles
13
+ const syntheticField = {
14
+ type: 'image',
15
+ slug: '_resolved',
16
+ styles: styles ?? []
17
+ };
18
+ const entries = await Promise.all(files.map(async (file) => {
19
+ const { styles: resolvedStyles, blurDataUrl } = await getImageStyles(syntheticField, file);
20
+ return [file.id, { data: file, styles: resolvedStyles, blurDataUrl }];
21
+ }));
22
+ return Object.fromEntries(entries);
23
+ }
@@ -1,2 +1,4 @@
1
1
  import type { Field } from '../../../types/fields.js';
2
+ import type { CustomFieldDefinition } from '../../../types/plugins.js';
3
+ export declare function setGeneratorCustomFields(customFields: Map<string, CustomFieldDefinition>): void;
2
4
  export declare function generateTsTypeFromFields(fields: Field[]): string;
@@ -1,4 +1,8 @@
1
1
  import { toPascalCase } from './utils.js';
2
+ let _customFields = new Map();
3
+ export function setGeneratorCustomFields(customFields) {
4
+ _customFields = customFields;
5
+ }
2
6
  function getFieldTypeAsString(field) {
3
7
  switch (field.type) {
4
8
  case 'text':
@@ -82,6 +86,10 @@ function getFieldTypeAsString(field) {
82
86
  urlParts.push('newTab?: boolean');
83
87
  return `{ ${urlParts.join('; ')} }`;
84
88
  }
89
+ case 'custom': {
90
+ const customDef = _customFields.get(field.fieldType);
91
+ return customDef?.tsType ?? 'unknown';
92
+ }
85
93
  default:
86
94
  return 'any';
87
95
  }
@@ -1,6 +1,6 @@
1
1
  import { mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { generateTsTypeFromFields } from './fields.js';
3
+ import { generateTsTypeFromFields, setGeneratorCustomFields } from './fields.js';
4
4
  import { generateTsTypeFromFormFields } from './formFields.js';
5
5
  import { generateZodSchemaStringFromFormFieldsAsString } from './formFieldSchemaToString.js';
6
6
  import { toPascalCase } from './utils.js';
@@ -203,6 +203,14 @@ function generateRemote(config) {
203
203
  writeFileSync(filePath, code);
204
204
  }
205
205
  export function generateRuntime(config) {
206
+ // Build custom fields map from plugins for type generation
207
+ const customFields = new Map();
208
+ for (const plugin of config.plugins ?? []) {
209
+ for (const field of plugin.fields ?? []) {
210
+ customFields.set(field.fieldType, field);
211
+ }
212
+ }
213
+ setGeneratorCustomFields(customFields);
206
214
  createCmsRuntimeDir();
207
215
  generateTypes(config);
208
216
  generateAPI(config);
@@ -1,3 +1,8 @@
1
+ import { drizzle } from 'drizzle-orm/postgres-js';
1
2
  import type { Config } from './types.js';
3
+ import * as schema from './schema/index.js';
2
4
  import type { DatabaseAdapter } from '../types/adapters/db.js';
3
- export declare function pg(config: Config): DatabaseAdapter;
5
+ export type DatabaseAdapterWithDrizzle = DatabaseAdapter & {
6
+ _drizzle: ReturnType<typeof drizzle<typeof schema>>;
7
+ };
8
+ export declare function pg(config: Config): DatabaseAdapterWithDrizzle;
@@ -88,6 +88,7 @@ export function pg(config) {
88
88
  const client = postgres(config.databaseUrl);
89
89
  const db = drizzle(client, { schema });
90
90
  return {
91
+ _drizzle: db,
91
92
  createEntry: async (data) => {
92
93
  return db.transaction(async (tx) => {
93
94
  const [newEntry] = await tx.insert(schema.entriesTable).values(data).returning();