@xleddyl/nuxt-cms 0.1.9

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 (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +26 -0
  3. package/dist/module.d.mts +36 -0
  4. package/dist/module.json +9 -0
  5. package/dist/module.mjs +765 -0
  6. package/dist/runtime/app/components/cms/BlocksField.d.vue.ts +15 -0
  7. package/dist/runtime/app/components/cms/BlocksField.vue +110 -0
  8. package/dist/runtime/app/components/cms/BlocksField.vue.d.ts +15 -0
  9. package/dist/runtime/app/components/cms/EmptyState.d.vue.ts +18 -0
  10. package/dist/runtime/app/components/cms/EmptyState.vue +20 -0
  11. package/dist/runtime/app/components/cms/EmptyState.vue.d.ts +18 -0
  12. package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +23 -0
  13. package/dist/runtime/app/components/cms/EntryForm.vue +78 -0
  14. package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +23 -0
  15. package/dist/runtime/app/components/cms/FieldInput.d.vue.ts +16 -0
  16. package/dist/runtime/app/components/cms/FieldInput.vue +85 -0
  17. package/dist/runtime/app/components/cms/FieldInput.vue.d.ts +16 -0
  18. package/dist/runtime/app/components/cms/JsonField.d.vue.ts +10 -0
  19. package/dist/runtime/app/components/cms/JsonField.vue +51 -0
  20. package/dist/runtime/app/components/cms/JsonField.vue.d.ts +10 -0
  21. package/dist/runtime/app/components/cms/MediaField.d.vue.ts +16 -0
  22. package/dist/runtime/app/components/cms/MediaField.vue +81 -0
  23. package/dist/runtime/app/components/cms/MediaField.vue.d.ts +16 -0
  24. package/dist/runtime/app/components/cms/MediaGallery.d.vue.ts +19 -0
  25. package/dist/runtime/app/components/cms/MediaGallery.vue +309 -0
  26. package/dist/runtime/app/components/cms/MediaGallery.vue.d.ts +19 -0
  27. package/dist/runtime/app/components/cms/MediaUpload.d.vue.ts +14 -0
  28. package/dist/runtime/app/components/cms/MediaUpload.vue +132 -0
  29. package/dist/runtime/app/components/cms/MediaUpload.vue.d.ts +14 -0
  30. package/dist/runtime/app/components/cms/PageHeader.d.vue.ts +19 -0
  31. package/dist/runtime/app/components/cms/PageHeader.vue +23 -0
  32. package/dist/runtime/app/components/cms/PageHeader.vue.d.ts +19 -0
  33. package/dist/runtime/app/components/cms/RelationField.d.vue.ts +16 -0
  34. package/dist/runtime/app/components/cms/RelationField.vue +87 -0
  35. package/dist/runtime/app/components/cms/RelationField.vue.d.ts +16 -0
  36. package/dist/runtime/app/components/cms/RichTextField.d.vue.ts +10 -0
  37. package/dist/runtime/app/components/cms/RichTextField.vue +123 -0
  38. package/dist/runtime/app/components/cms/RichTextField.vue.d.ts +10 -0
  39. package/dist/runtime/app/components/cms/SlugField.d.vue.ts +14 -0
  40. package/dist/runtime/app/components/cms/SlugField.vue +44 -0
  41. package/dist/runtime/app/components/cms/SlugField.vue.d.ts +14 -0
  42. package/dist/runtime/app/components/cms/TranslatableField.d.vue.ts +15 -0
  43. package/dist/runtime/app/components/cms/TranslatableField.vue +64 -0
  44. package/dist/runtime/app/components/cms/TranslatableField.vue.d.ts +15 -0
  45. package/dist/runtime/app/composables/cms-query.d.ts +4 -0
  46. package/dist/runtime/app/composables/cms-query.js +18 -0
  47. package/dist/runtime/app/composables/cms-runtime.d.ts +5 -0
  48. package/dist/runtime/app/composables/cms-runtime.js +4 -0
  49. package/dist/runtime/app/i18n/en.json +93 -0
  50. package/dist/runtime/app/i18n/it.json +93 -0
  51. package/dist/runtime/app/layouts/cms-admin.d.vue.ts +13 -0
  52. package/dist/runtime/app/layouts/cms-admin.vue +97 -0
  53. package/dist/runtime/app/layouts/cms-admin.vue.d.ts +13 -0
  54. package/dist/runtime/app/middleware/cms-auth.d.ts +2 -0
  55. package/dist/runtime/app/middleware/cms-auth.js +7 -0
  56. package/dist/runtime/app/pages/admin-collection.d.vue.ts +3 -0
  57. package/dist/runtime/app/pages/admin-collection.vue +273 -0
  58. package/dist/runtime/app/pages/admin-collection.vue.d.ts +3 -0
  59. package/dist/runtime/app/pages/admin-entry.d.vue.ts +3 -0
  60. package/dist/runtime/app/pages/admin-entry.vue +156 -0
  61. package/dist/runtime/app/pages/admin-entry.vue.d.ts +3 -0
  62. package/dist/runtime/app/pages/admin-index.d.vue.ts +3 -0
  63. package/dist/runtime/app/pages/admin-index.vue +11 -0
  64. package/dist/runtime/app/pages/admin-index.vue.d.ts +3 -0
  65. package/dist/runtime/app/pages/admin-login.d.vue.ts +3 -0
  66. package/dist/runtime/app/pages/admin-login.vue +87 -0
  67. package/dist/runtime/app/pages/admin-login.vue.d.ts +3 -0
  68. package/dist/runtime/app/pages/admin-media.d.vue.ts +3 -0
  69. package/dist/runtime/app/pages/admin-media.vue +13 -0
  70. package/dist/runtime/app/pages/admin-media.vue.d.ts +3 -0
  71. package/dist/runtime/app/utils/api.d.ts +7 -0
  72. package/dist/runtime/app/utils/api.js +3 -0
  73. package/dist/runtime/app/utils/ui.d.ts +8 -0
  74. package/dist/runtime/app/utils/ui.js +9 -0
  75. package/dist/runtime/assets/main.css +1 -0
  76. package/dist/runtime/server/api/collection.get.d.ts +6 -0
  77. package/dist/runtime/server/api/collection.get.js +19 -0
  78. package/dist/runtime/server/api/collection.post.d.ts +2 -0
  79. package/dist/runtime/server/api/collection.post.js +30 -0
  80. package/dist/runtime/server/api/collection.put.d.ts +2 -0
  81. package/dist/runtime/server/api/collection.put.js +28 -0
  82. package/dist/runtime/server/api/item.delete.d.ts +4 -0
  83. package/dist/runtime/server/api/item.delete.js +19 -0
  84. package/dist/runtime/server/api/item.get.d.ts +2 -0
  85. package/dist/runtime/server/api/item.get.js +18 -0
  86. package/dist/runtime/server/api/item.put.d.ts +2 -0
  87. package/dist/runtime/server/api/item.put.js +40 -0
  88. package/dist/runtime/server/api/media-presign.post.d.ts +11 -0
  89. package/dist/runtime/server/api/media-presign.post.js +51 -0
  90. package/dist/runtime/server/api/media.delete.d.ts +4 -0
  91. package/dist/runtime/server/api/media.delete.js +25 -0
  92. package/dist/runtime/server/api/media.get.d.ts +5 -0
  93. package/dist/runtime/server/api/media.get.js +13 -0
  94. package/dist/runtime/server/api/media.post.d.ts +3 -0
  95. package/dist/runtime/server/api/media.post.js +32 -0
  96. package/dist/runtime/server/api/media.put.d.ts +3 -0
  97. package/dist/runtime/server/api/media.put.js +28 -0
  98. package/dist/runtime/server/plugins/migrate-postgres.d.ts +2 -0
  99. package/dist/runtime/server/plugins/migrate-postgres.js +9 -0
  100. package/dist/runtime/server/plugins/migrate-sqlite.d.ts +2 -0
  101. package/dist/runtime/server/plugins/migrate-sqlite.js +9 -0
  102. package/dist/runtime/server/plugins/session-check.d.ts +2 -0
  103. package/dist/runtime/server/plugins/session-check.js +9 -0
  104. package/dist/runtime/server/routes/auth/login.post.d.ts +4 -0
  105. package/dist/runtime/server/routes/auth/login.post.js +62 -0
  106. package/dist/runtime/server/routes/graphql.d.ts +2 -0
  107. package/dist/runtime/server/routes/graphql.js +34 -0
  108. package/dist/runtime/server/tsconfig.json +3 -0
  109. package/dist/runtime/server/utils/custom-id.d.ts +2 -0
  110. package/dist/runtime/server/utils/custom-id.js +10 -0
  111. package/dist/runtime/server/utils/db-errors.d.ts +1 -0
  112. package/dist/runtime/server/utils/db-errors.js +31 -0
  113. package/dist/runtime/server/utils/db-postgres.d.ts +3 -0
  114. package/dist/runtime/server/utils/db-postgres.js +16 -0
  115. package/dist/runtime/server/utils/db-sqlite.d.ts +4 -0
  116. package/dist/runtime/server/utils/db-sqlite.js +17 -0
  117. package/dist/runtime/server/utils/graphql-depth.d.ts +4 -0
  118. package/dist/runtime/server/utils/graphql-depth.js +72 -0
  119. package/dist/runtime/server/utils/graphql.d.ts +14 -0
  120. package/dist/runtime/server/utils/graphql.js +317 -0
  121. package/dist/runtime/server/utils/media.d.ts +24 -0
  122. package/dist/runtime/server/utils/media.js +38 -0
  123. package/dist/runtime/server/utils/registry.d.ts +20 -0
  124. package/dist/runtime/server/utils/registry.js +47 -0
  125. package/dist/runtime/server/utils/relations.d.ts +12 -0
  126. package/dist/runtime/server/utils/relations.js +80 -0
  127. package/dist/runtime/server/utils/require-admin.d.ts +2 -0
  128. package/dist/runtime/server/utils/require-admin.js +13 -0
  129. package/dist/runtime/shared/empty-config.d.ts +2 -0
  130. package/dist/runtime/shared/empty-config.js +1 -0
  131. package/dist/runtime/shared/graphql-sdl.d.ts +5 -0
  132. package/dist/runtime/shared/graphql-sdl.js +147 -0
  133. package/dist/runtime/shared/index.d.ts +62 -0
  134. package/dist/runtime/shared/index.js +55 -0
  135. package/dist/runtime/shared/validation.d.ts +16 -0
  136. package/dist/runtime/shared/validation.js +82 -0
  137. package/dist/types.d.mts +3 -0
  138. package/package.json +93 -0
@@ -0,0 +1,765 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import { isAbsolute, resolve, relative, join, dirname } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { defineNuxtModule, createResolver, useLogger, resolvePath, addTemplate, addImports, addServerPlugin, addTypeTemplate, addLayout, addComponentsDir, addRouteMiddleware, extendPages, addServerHandler } from '@nuxt/kit';
7
+ import { introspectionFromSchema, buildSchema } from 'graphql';
8
+ import { minifyIntrospection, outputIntrospectionFile } from 'gql.tada/internal';
9
+ import { createJiti } from 'jiti';
10
+ import { typeName, blockTypeName, blockUnionName, renderGraphqlSdl } from '../dist/runtime/shared/graphql-sdl.js';
11
+ import { isTranslatableField } from '../dist/runtime/shared/index.js';
12
+
13
+ const IDENTIFIER = /^[a-z_]\w*$/i;
14
+ const RESERVED_ENTRY_KEYS = ["admin", "auth", "login", "media", "graphql", "cms_media"];
15
+ const RESERVED_COLUMNS = ["id", "status", "created_at", "updated_at"];
16
+ const RESERVED_TYPE_NAMES = [
17
+ "Query",
18
+ "Mutation",
19
+ "Subscription",
20
+ "CmsMedia",
21
+ "JSON",
22
+ "SortDirection",
23
+ "IntFilter",
24
+ "FloatFilter",
25
+ "StringFilter",
26
+ "BooleanFilter"
27
+ ];
28
+ function validateConfig(config, i18n) {
29
+ const errors = [];
30
+ const locales = i18n?.locales ?? [];
31
+ if (locales.length && !locales.includes(i18n.defaultLocale)) {
32
+ errors.push(
33
+ `cms.i18n: defaultLocale '${i18n.defaultLocale}' is not in locales [${locales.join(
34
+ ", "
35
+ )}]`
36
+ );
37
+ }
38
+ const entryIds = /* @__PURE__ */ new Map();
39
+ const typeNames = /* @__PURE__ */ new Map();
40
+ for (const [name, entry] of Object.entries(config)) {
41
+ const at = `cms.config entry '${name}'`;
42
+ if (!IDENTIFIER.test(name)) errors.push(`${at}: key must be a valid identifier`);
43
+ if (RESERVED_ENTRY_KEYS.includes(name)) errors.push(`${at}: '${name}' is a reserved name`);
44
+ const gqlType = typeName(name);
45
+ if (RESERVED_TYPE_NAMES.includes(gqlType)) {
46
+ errors.push(`${at}: GraphQL type name '${gqlType}' is reserved`);
47
+ } else if (typeNames.has(gqlType)) {
48
+ errors.push(
49
+ `${at}: GraphQL type name '${gqlType}' clashes with entry '${typeNames.get(gqlType)}'`
50
+ );
51
+ } else {
52
+ typeNames.set(gqlType, name);
53
+ }
54
+ if (!entry.id) {
55
+ errors.push(`${at}: id is required`);
56
+ } else if (!IDENTIFIER.test(entry.id)) {
57
+ errors.push(`${at}: id '${entry.id}' must be a valid identifier`);
58
+ } else if (entryIds.has(entry.id)) {
59
+ errors.push(`${at}: id '${entry.id}' is already used by entry '${entryIds.get(entry.id)}'`);
60
+ } else {
61
+ entryIds.set(entry.id, name);
62
+ }
63
+ if (entry.kind !== "collection" && entry.kind !== "single")
64
+ errors.push(`${at}: kind must be 'collection' or 'single'`);
65
+ if (entry.drafts && entry.kind !== "collection")
66
+ errors.push(`${at}: drafts are only supported on collections`);
67
+ if (!entry.fields || !Object.keys(entry.fields).length)
68
+ errors.push(`${at}: fields must not be empty`);
69
+ if (entry.titleField && !entry.fields?.[entry.titleField])
70
+ errors.push(`${at}: titleField '${entry.titleField}' is not a declared field`);
71
+ const columnNames = /* @__PURE__ */ new Set();
72
+ for (const [key, field] of Object.entries(entry.fields ?? {})) {
73
+ const fat = `${at}, field '${key}'`;
74
+ if (!IDENTIFIER.test(key)) errors.push(`${fat}: key must be a valid identifier`);
75
+ const column = snakeCase(key);
76
+ if (RESERVED_COLUMNS.includes(column))
77
+ errors.push(`${fat}: column '${column}' is reserved`);
78
+ if (!isManyToMany(field)) {
79
+ if (columnNames.has(column))
80
+ errors.push(`${fat}: column '${column}' clashes with another field`);
81
+ columnNames.add(column);
82
+ }
83
+ if (field.translatable) {
84
+ if (field.type !== "text" && field.type !== "richtext")
85
+ errors.push(`${fat}: translatable is only supported on text and richtext fields`);
86
+ if (!locales.length)
87
+ errors.push(`${fat}: translatable requires cms.i18n.locales in nuxt.config`);
88
+ }
89
+ if (field.type === "select") {
90
+ if (!field.options?.length)
91
+ errors.push(`${fat}: select requires a non-empty options array`);
92
+ else if (new Set(field.options).size !== field.options.length)
93
+ errors.push(`${fat}: select options must be unique`);
94
+ }
95
+ if (field.type === "slug" && field.from) {
96
+ const source = entry.fields?.[field.from];
97
+ if (!source) errors.push(`${fat}: slug source '${field.from}' is not a declared field`);
98
+ else if (source.type !== "text")
99
+ errors.push(`${fat}: slug source '${field.from}' must be a text field`);
100
+ else if (isTranslatableField(source))
101
+ errors.push(`${fat}: slug source '${field.from}' cannot be translatable`);
102
+ }
103
+ if (field.type === "blocks") {
104
+ if (field.translatable) errors.push(`${fat}: blocks fields cannot be translatable`);
105
+ if (!field.blocks || !Object.keys(field.blocks).length) {
106
+ errors.push(`${fat}: blocks requires a non-empty blocks map`);
107
+ }
108
+ for (const [blockName, block] of Object.entries(field.blocks ?? {})) {
109
+ const bat = `${fat}, block '${blockName}'`;
110
+ if (!IDENTIFIER.test(blockName))
111
+ errors.push(`${bat}: key must be a valid identifier`);
112
+ if (!block.fields || !Object.keys(block.fields).length)
113
+ errors.push(`${bat}: fields must not be empty`);
114
+ for (const [blockFieldKey, blockField] of Object.entries(block.fields ?? {})) {
115
+ const bfat = `${bat}, field '${blockFieldKey}'`;
116
+ if (!IDENTIFIER.test(blockFieldKey))
117
+ errors.push(`${bfat}: key must be a valid identifier`);
118
+ if (blockFieldKey === "type")
119
+ errors.push(`${bfat}: 'type' is reserved inside blocks`);
120
+ if (["relation", "blocks", "slug"].includes(blockField.type)) {
121
+ errors.push(
122
+ `${bfat}: ${blockField.type} fields are not supported inside blocks`
123
+ );
124
+ }
125
+ if (blockField.translatable)
126
+ errors.push(`${bfat}: translatable fields are not supported inside blocks`);
127
+ if (blockField.type === "select" && !blockField.options?.length) {
128
+ errors.push(`${bfat}: select requires a non-empty options array`);
129
+ }
130
+ }
131
+ }
132
+ }
133
+ if (field.type === "relation") {
134
+ const target = field.to ? config[field.to] : void 0;
135
+ if (!field.to || !target) {
136
+ errors.push(`${fat}: relation target '${field.to}' is not in the registry`);
137
+ } else {
138
+ if (target.kind !== "collection")
139
+ errors.push(`${fat}: relation target '${field.to}' must be a collection`);
140
+ if (!entry.table && target.table)
141
+ errors.push(
142
+ `${fat}: relation target '${field.to}' uses a custom table \u2014 derived relations can only target derived entries`
143
+ );
144
+ }
145
+ if (field.cardinality && !["many-to-one", "one-to-one", "many-to-many"].includes(field.cardinality)) {
146
+ errors.push(`${fat}: unknown cardinality '${field.cardinality}'`);
147
+ }
148
+ if (field.required && field.onDelete === "set null")
149
+ errors.push(`${fat}: a required relation cannot use onDelete 'set null'`);
150
+ if (field.cardinality === "many-to-many") {
151
+ if (field.onDelete === "set null")
152
+ errors.push(`${fat}: many-to-many does not accept onDelete 'set null'`);
153
+ if (entry.table)
154
+ errors.push(`${fat}: many-to-many is not available on custom-table entries`);
155
+ if (config[`${name}_${key}`])
156
+ errors.push(`${fat}: '${name}_${key}' clashes with the derived join table name`);
157
+ }
158
+ }
159
+ }
160
+ }
161
+ const sqlTableNames = /* @__PURE__ */ new Map([["cms_media", "the built-in media table"]]);
162
+ for (const [name, entry] of Object.entries(config)) {
163
+ if (entry.table) continue;
164
+ const registerTable = (sqlName, source) => {
165
+ const existing = sqlTableNames.get(sqlName);
166
+ if (existing) {
167
+ errors.push(
168
+ `cms.config: table name '${sqlName}' from ${source} clashes with ${existing}`
169
+ );
170
+ } else {
171
+ sqlTableNames.set(sqlName, source);
172
+ }
173
+ };
174
+ registerTable(name, `entry '${name}'`);
175
+ for (const [key, field] of Object.entries(entry.fields ?? {})) {
176
+ if (isManyToMany(field))
177
+ registerTable(`${name}_${snakeCase(key)}`, `the join table of '${name}.${key}'`);
178
+ }
179
+ }
180
+ return errors;
181
+ }
182
+ function snakeCase(key) {
183
+ return key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
184
+ }
185
+ function jsonExpr(col, dialect) {
186
+ return dialect === "postgres" ? `jsonb('${col}')` : `text('${col}', { mode: 'json' })`;
187
+ }
188
+ function columnExpr(key, field, dialect) {
189
+ const col = snakeCase(key);
190
+ let expr;
191
+ if (isTranslatableField(field)) {
192
+ expr = jsonExpr(col, dialect);
193
+ if (field.required) expr += ".notNull()";
194
+ return ` ${key}: ${expr},`;
195
+ }
196
+ switch (field.type) {
197
+ case "number":
198
+ expr = field.integer ? `integer('${col}')` : dialect === "postgres" ? `doublePrecision('${col}')` : `real('${col}')`;
199
+ break;
200
+ case "boolean":
201
+ expr = dialect === "postgres" ? `boolean('${col}')` : `integer('${col}', { mode: 'boolean' })`;
202
+ break;
203
+ case "date":
204
+ expr = dialect === "postgres" ? `date('${col}', { mode: 'string' })` : `text('${col}')`;
205
+ break;
206
+ case "json":
207
+ case "blocks":
208
+ expr = jsonExpr(col, dialect);
209
+ break;
210
+ case "slug":
211
+ expr = `text('${col}').unique()`;
212
+ break;
213
+ case "relation": {
214
+ const onDelete = field.onDelete ?? (field.required ? "restrict" : "set null");
215
+ expr = `text('${col}').references(() => ${field.to}.id, { onDelete: '${onDelete}' })`;
216
+ if (field.cardinality === "one-to-one") expr += ".unique()";
217
+ break;
218
+ }
219
+ default:
220
+ expr = `text('${col}')`;
221
+ }
222
+ if (field.required) expr += ".notNull()";
223
+ return ` ${key}: ${expr},`;
224
+ }
225
+ function isManyToMany(field) {
226
+ return field.type === "relation" && field.cardinality === "many-to-many";
227
+ }
228
+ function autoIdExpr(pg) {
229
+ return pg ? `integer('id').primaryKey().generatedAlwaysAsIdentity()` : `integer('id').primaryKey({ autoIncrement: true })`;
230
+ }
231
+ function nowExpr(pg, col) {
232
+ return pg ? `timestamp('${col}', { mode: 'string' }).notNull().defaultNow()` : `text('${col}').notNull().default(sql\`(datetime('now'))\`)`;
233
+ }
234
+ function tableExpr(name, entry, dialect) {
235
+ const pg = dialect === "postgres";
236
+ const tableFn = pg ? "pgTable" : "sqliteTable";
237
+ const lines = [];
238
+ lines.push(` id: text('id').primaryKey(),`);
239
+ for (const [key, field] of Object.entries(entry.fields)) {
240
+ if (isManyToMany(field)) continue;
241
+ lines.push(columnExpr(key, field, dialect));
242
+ }
243
+ if (entry.drafts) lines.push(` status: text('status').notNull().default('draft'),`);
244
+ if (entry.kind === "collection") lines.push(` createdAt: ${nowExpr(pg, "created_at")},`);
245
+ lines.push(` updatedAt: ${nowExpr(pg, "updated_at")},`);
246
+ return `export const ${name} = ${tableFn}('${name}', {
247
+ ${lines.join("\n")}
248
+ })`;
249
+ }
250
+ function joinTableExpr(name, key, field, dialect) {
251
+ const pg = dialect === "postgres";
252
+ const tableFn = pg ? "pgTable" : "sqliteTable";
253
+ const joinName = `${name}_${snakeCase(key)}`;
254
+ const targetOnDelete = field.onDelete ?? "cascade";
255
+ return `export const ${name}_${key} = ${tableFn}('${joinName}', {
256
+ sourceId: text('source_id').notNull().references(() => ${name}.id, { onDelete: 'cascade' }),
257
+ targetId: text('target_id').notNull().references(() => ${field.to}.id, { onDelete: '${targetOnDelete}' }),
258
+ position: integer('position').notNull().default(0),
259
+ }, table => [primaryKey({ columns: [table.sourceId, table.targetId] })])`;
260
+ }
261
+ function mediaTableExpr(dialect) {
262
+ const pg = dialect === "postgres";
263
+ const tableFn = pg ? "pgTable" : "sqliteTable";
264
+ return `export const cms_media = ${tableFn}('cms_media', {
265
+ id: ${autoIdExpr(pg)},
266
+ key: text('key').notNull().unique(),
267
+ alt: text('alt'),
268
+ folder: text('folder'),
269
+ mime: text('mime'),
270
+ size: integer('size'),
271
+ width: integer('width'),
272
+ height: integer('height'),
273
+ createdAt: ${nowExpr(pg, "created_at")},
274
+ })`;
275
+ }
276
+ function renderSchemaFile(config, dialect, resolveImport = (s) => s) {
277
+ const derived = Object.entries(config).filter(([, entry]) => !entry.table);
278
+ const fields = derived.flatMap(([, entry]) => Object.values(entry.fields));
279
+ const pg = dialect === "postgres";
280
+ const core = /* @__PURE__ */ new Set(["integer", "text", pg ? "pgTable" : "sqliteTable"]);
281
+ if (fields.some((f) => f.type === "number" && !f.integer))
282
+ core.add(pg ? "doublePrecision" : "real");
283
+ if (pg && fields.some((f) => f.type === "date")) core.add("date");
284
+ if (pg && fields.some((f) => f.type === "boolean")) core.add("boolean");
285
+ if (pg && fields.some((f) => f.type === "json" || f.type === "blocks" || isTranslatableField(f)))
286
+ core.add("jsonb");
287
+ if (pg) core.add("timestamp");
288
+ if (fields.some(isManyToMany)) core.add("primaryKey");
289
+ const imports = [
290
+ ...pg ? [] : [`import { sql } from '${resolveImport("drizzle-orm")}'`],
291
+ `import { ${[...core].sort().join(", ")} } from '${resolveImport(
292
+ `drizzle-orm/${pg ? "pg-core" : "sqlite-core"}`
293
+ )}'`
294
+ ];
295
+ const tables = derived.map(([name, entry]) => tableExpr(name, entry, dialect));
296
+ const joins = derived.flatMap(
297
+ ([name, entry]) => Object.entries(entry.fields).filter(([, field]) => isManyToMany(field)).map(([key, field]) => joinTableExpr(name, key, field, dialect))
298
+ );
299
+ return `${imports.join("\n")}
300
+
301
+ ${[mediaTableExpr(dialect), ...tables, ...joins].join(
302
+ "\n\n"
303
+ )}
304
+ `;
305
+ }
306
+
307
+ function scalarTsType(field) {
308
+ switch (field.type) {
309
+ case "number":
310
+ return "number";
311
+ case "boolean":
312
+ return "boolean";
313
+ case "json":
314
+ return "unknown";
315
+ case "select":
316
+ return field.options.map((o) => JSON.stringify(o)).join(" | ");
317
+ default:
318
+ return "string";
319
+ }
320
+ }
321
+ function fieldTsType(config, entryName, key, field) {
322
+ if (field.type === "relation") {
323
+ const target = typeName(field.to);
324
+ if (field.cardinality === "many-to-many") return `${target}[]`;
325
+ return field.required && !config[field.to]?.drafts ? target : `${target} | null`;
326
+ }
327
+ if (field.type === "media") return "CmsMedia | null";
328
+ if (field.type === "blocks") {
329
+ const union = blockUnionName(entryName, key);
330
+ return field.required ? `${union}[]` : `${union}[] | null`;
331
+ }
332
+ if (isTranslatableField(field)) return field.required ? "string" : "string | null";
333
+ const base = scalarTsType(field);
334
+ return field.required ? base : `${base} | null`;
335
+ }
336
+ function blockTypesTs(entryName, key, field) {
337
+ const defs = [];
338
+ const members = [];
339
+ for (const [blockName, block] of Object.entries(field.blocks ?? {})) {
340
+ const name = blockTypeName(entryName, key, blockName);
341
+ members.push(name);
342
+ const lines = [` __typename?: '${name}'`, ` type: '${blockName}'`];
343
+ for (const [blockFieldKey, blockField] of Object.entries(block.fields)) {
344
+ if (blockField.type === "media") {
345
+ lines.push(` ${blockFieldKey}: CmsMedia | null`);
346
+ continue;
347
+ }
348
+ const base = scalarTsType(blockField);
349
+ lines.push(` ${blockFieldKey}: ${blockField.required ? base : `${base} | null`}`);
350
+ }
351
+ defs.push(`export interface ${name} {
352
+ ${lines.join("\n")}
353
+ }`);
354
+ }
355
+ if (members.length)
356
+ defs.push(`export type ${blockUnionName(entryName, key)} = ${members.join(" | ")}`);
357
+ return defs;
358
+ }
359
+ function entryTs(config, name, entry) {
360
+ const lines = [" id: string"];
361
+ for (const [key, field] of Object.entries(entry.fields)) {
362
+ lines.push(` ${key}: ${fieldTsType(config, name, key, field)}`);
363
+ }
364
+ if (entry.kind === "collection") lines.push(" createdAt: string");
365
+ lines.push(" updatedAt: string");
366
+ return `export interface ${typeName(name)} {
367
+ ${lines.join("\n")}
368
+ }`;
369
+ }
370
+ function renderTypesFile(config) {
371
+ const parts = [
372
+ `export interface CmsMedia {
373
+ key: string
374
+ url: string | null
375
+ type: 'image' | 'video' | 'file'
376
+ alt: string | null
377
+ folder: string | null
378
+ mime: string | null
379
+ size: number | null
380
+ width: number | null
381
+ height: number | null
382
+ }`
383
+ ];
384
+ for (const [name, entry] of Object.entries(config)) {
385
+ for (const [key, field] of Object.entries(entry.fields)) {
386
+ if (field.type === "blocks") parts.push(...blockTypesTs(name, key, field));
387
+ }
388
+ parts.push(entryTs(config, name, entry));
389
+ }
390
+ return `${parts.join("\n\n")}
391
+ `;
392
+ }
393
+
394
+ const module$1 = defineNuxtModule({
395
+ meta: {
396
+ name: "@xleddyl/nuxt-cms",
397
+ configKey: "cms"
398
+ },
399
+ moduleDependencies: {
400
+ "@nuxt/ui": {},
401
+ "nuxt-auth-utils": {},
402
+ "@nuxtjs/i18n": {
403
+ defaults: {
404
+ locales: [
405
+ { code: "en", name: "English" },
406
+ { code: "it", name: "Italiano" }
407
+ ],
408
+ defaultLocale: "en",
409
+ strategy: "no_prefix",
410
+ detectBrowserLanguage: {
411
+ useCookie: true,
412
+ cookieKey: "cms-locale",
413
+ fallbackLocale: "en"
414
+ }
415
+ }
416
+ }
417
+ },
418
+ defaults: {
419
+ configPath: "cms.config",
420
+ admin: {
421
+ email: "",
422
+ password: ""
423
+ },
424
+ database: {
425
+ driver: "sqlite",
426
+ path: "data/cms.db",
427
+ url: ""
428
+ },
429
+ media: {
430
+ endpoint: "",
431
+ region: "auto",
432
+ bucket: "",
433
+ publicBaseUrl: "",
434
+ presignExpiry: 600,
435
+ accessKeyId: "",
436
+ secretAccessKey: ""
437
+ },
438
+ i18n: {
439
+ locales: [],
440
+ defaultLocale: "en"
441
+ },
442
+ graphql: {
443
+ maxDepth: 8
444
+ }
445
+ },
446
+ async setup(options, nuxt) {
447
+ const resolver = createResolver(import.meta.url);
448
+ const logger = useLogger("nuxt-cms");
449
+ const configPath = await resolvePath(options.configPath, { cwd: nuxt.options.rootDir });
450
+ nuxt.options.alias["#nuxt-cms"] = resolver.resolve("./runtime/shared/index");
451
+ nuxt.options.watch.push(configPath);
452
+ let cmsConfig = {};
453
+ if (existsSync(configPath)) {
454
+ nuxt.options.alias["#cms-config"] = configPath;
455
+ const jiti = createJiti(import.meta.url, {
456
+ moduleCache: false,
457
+ alias: { "#nuxt-cms": resolver.resolve("./runtime/shared/index") }
458
+ });
459
+ cmsConfig = await jiti.import(configPath, { default: true });
460
+ } else {
461
+ logger.warn(
462
+ `[nuxt-cms] Config file not found: ${configPath}. Using an empty registry \u2014 create a ${options.configPath}.ts with defineCmsConfig().`
463
+ );
464
+ nuxt.options.alias["#cms-config"] = resolver.resolve("./runtime/shared/empty-config");
465
+ }
466
+ const configErrors = validateConfig(cmsConfig, options.i18n);
467
+ if (configErrors.length) {
468
+ for (const error of configErrors) logger.error(error);
469
+ throw new Error(
470
+ `[nuxt-cms] Invalid cms config (${configErrors.length} error${configErrors.length > 1 ? "s" : ""})`
471
+ );
472
+ }
473
+ const moduleRequire = createRequire(import.meta.url);
474
+ const resolveImport = (specifier) => {
475
+ try {
476
+ return fileURLToPath(import.meta.resolve(specifier)).replace(/\\/g, "/");
477
+ } catch {
478
+ return moduleRequire.resolve(specifier).replace(/\\/g, "/");
479
+ }
480
+ };
481
+ const schemaTemplate = addTemplate({
482
+ filename: "cms/schema.ts",
483
+ write: true,
484
+ getContents: () => renderSchemaFile(cmsConfig, options.database.driver ?? "sqlite", resolveImport)
485
+ });
486
+ nuxt.options.alias["#cms-tables"] = schemaTemplate.dst;
487
+ addTemplate({
488
+ filename: "cms/schema.graphql",
489
+ write: true,
490
+ getContents: () => renderGraphqlSdl(cmsConfig)
491
+ });
492
+ const typesTemplate = addTemplate({
493
+ filename: "cms/types.ts",
494
+ write: true,
495
+ getContents: () => renderTypesFile(cmsConfig)
496
+ });
497
+ nuxt.options.alias["#cms-types"] = typesTemplate.dst;
498
+ addTemplate({
499
+ filename: "cms/graphql-env.d.ts",
500
+ write: true,
501
+ getContents: () => {
502
+ const introspection = minifyIntrospection(
503
+ introspectionFromSchema(buildSchema(renderGraphqlSdl(cmsConfig)))
504
+ );
505
+ return outputIntrospectionFile(introspection, {
506
+ fileType: ".d.ts",
507
+ shouldPreprocess: true
508
+ }).split("import * as gqlTada from 'gql.tada';")[0];
509
+ }
510
+ });
511
+ const gqlTadaTypesPath = resolveImport("gql.tada").replace(/\.[mc]?js$/, "");
512
+ const graphqlTemplate = addTemplate({
513
+ filename: "cms/graphql.ts",
514
+ write: true,
515
+ getContents: () => [
516
+ `import type { initGraphQLTada, ResultOf, VariablesOf } from '${gqlTadaTypesPath}'`,
517
+ `import type { introspection } from './graphql-env'`,
518
+ ``,
519
+ `export type CmsGraphql = initGraphQLTada<{`,
520
+ ` introspection: introspection`,
521
+ ` scalars: {`,
522
+ ` JSON: unknown`,
523
+ ` }`,
524
+ `}>`,
525
+ ``,
526
+ `declare const graphql: CmsGraphql`,
527
+ ``,
528
+ `// @ts-ignore: gql.tada's cache overload rejects this instantiation, but the parse overload still resolves`,
529
+ `export type CmsDocument<Query extends string> = ReturnType<typeof graphql<Query, []>>`,
530
+ ``,
531
+ `export type CmsResult<Query extends string> = string extends Query`,
532
+ ` ? Record<string, unknown>`,
533
+ ` : ResultOf<CmsDocument<Query>>`,
534
+ ``,
535
+ `export type CmsVariables<Query extends string> = string extends Query`,
536
+ ` ? Record<string, unknown>`,
537
+ ` : VariablesOf<CmsDocument<Query>>`,
538
+ ``
539
+ ].join("\n")
540
+ });
541
+ nuxt.options.alias["#cms-graphql"] = graphqlTemplate.dst;
542
+ addImports([
543
+ { name: "useCms", from: resolver.resolve("./runtime/app/composables/cms-query") },
544
+ { name: "$cmsQuery", from: resolver.resolve("./runtime/app/composables/cms-query") }
545
+ ]);
546
+ nuxt.hook("i18n:registerModule", (register) => {
547
+ register({
548
+ langDir: resolver.resolve("./runtime/app/i18n"),
549
+ locales: [
550
+ { code: "en", file: "en.json" },
551
+ { code: "it", file: "it.json" }
552
+ ]
553
+ });
554
+ });
555
+ const { driver = "sqlite", path: dbPath = "data/cms.db", url: databaseUrl = "" } = options.database;
556
+ nuxt.options.alias["#cms-db"] = resolver.resolve(
557
+ driver === "postgres" ? "./runtime/server/utils/db-postgres" : "./runtime/server/utils/db-sqlite"
558
+ );
559
+ const dialect = driver === "postgres" ? "postgresql" : "sqlite";
560
+ const resolvedDbPath = isAbsolute(dbPath) ? dbPath : resolve(nuxt.options.rootDir, dbPath);
561
+ const migrationsDir = resolve(nuxt.options.rootDir, `server/db/migrations/${driver}`);
562
+ const relativeSchemaPath = relative(nuxt.options.rootDir, schemaTemplate.dst);
563
+ const relativeMigrationsDir = relative(nuxt.options.rootDir, migrationsDir);
564
+ const toPosix = (path) => path.replace(/\\/g, "/");
565
+ addTemplate({
566
+ filename: "cms/drizzle.config.ts",
567
+ write: true,
568
+ getContents: () => [
569
+ `export default {`,
570
+ ` dialect: '${dialect}',`,
571
+ ` schema: '${toPosix(schemaTemplate.dst)}',`,
572
+ ` out: '${toPosix(migrationsDir)}',`,
573
+ driver === "postgres" ? ` dbCredentials: { url: process.env.NUXT_CMS_DATABASE_URL ?? '${databaseUrl}' },` : ` dbCredentials: { url: '${toPosix(resolvedDbPath)}' },`,
574
+ `}`,
575
+ ``
576
+ ].join("\n")
577
+ });
578
+ addServerPlugin(
579
+ resolver.resolve(
580
+ driver === "postgres" ? "./runtime/server/plugins/migrate-postgres" : "./runtime/server/plugins/migrate-sqlite"
581
+ )
582
+ );
583
+ if (!nuxt.options.dev) {
584
+ addServerPlugin(resolver.resolve("./runtime/server/plugins/session-check"));
585
+ }
586
+ if (nuxt.options.dev) {
587
+ let generated = false;
588
+ nuxt.hook("app:templatesGenerated", async () => {
589
+ if (generated) return;
590
+ generated = true;
591
+ const bin = join(dirname(moduleRequire.resolve("drizzle-kit")), "bin.cjs");
592
+ const args = [
593
+ bin,
594
+ "generate",
595
+ `--dialect=${dialect}`,
596
+ `--schema=${relativeSchemaPath}`,
597
+ `--out=${relativeMigrationsDir}`
598
+ ];
599
+ const code = await new Promise((done) => {
600
+ spawn(process.execPath, args, { cwd: nuxt.options.rootDir, stdio: "inherit" }).on("close", (exitCode) => done(exitCode ?? 1)).on("error", () => done(1));
601
+ });
602
+ if (code !== 0)
603
+ logger.error(`[nuxt-cms] drizzle-kit generate failed (exit code ${code})`);
604
+ });
605
+ }
606
+ const existingConfig = nuxt.options.runtimeConfig.cms ?? {};
607
+ nuxt.options.runtimeConfig.cms = {
608
+ adminEmail: options.admin.email,
609
+ adminPassword: options.admin.password,
610
+ databaseUrl,
611
+ dbPath: resolvedDbPath,
612
+ migrationsDir,
613
+ ...existingConfig,
614
+ graphql: {
615
+ graphiql: nuxt.options.dev,
616
+ maxDepth: options.graphql.maxDepth,
617
+ ...existingConfig.graphql ?? {}
618
+ },
619
+ media: {
620
+ endpoint: options.media.endpoint,
621
+ region: options.media.region,
622
+ bucket: options.media.bucket,
623
+ presignExpiry: options.media.presignExpiry,
624
+ accessKeyId: options.media.accessKeyId,
625
+ secretAccessKey: options.media.secretAccessKey,
626
+ ...existingConfig.media ?? {}
627
+ }
628
+ };
629
+ nuxt.options.runtimeConfig.public.cms = {
630
+ mediaBaseUrl: options.media.publicBaseUrl,
631
+ i18n: options.i18n
632
+ };
633
+ addTypeTemplate(
634
+ {
635
+ filename: "types/nuxt-cms-auth.d.ts",
636
+ getContents: () => [
637
+ "import type { H3Event } from 'h3'",
638
+ "",
639
+ "declare module '#auth-utils' {",
640
+ " interface User {",
641
+ " email: string",
642
+ " }",
643
+ "}",
644
+ "",
645
+ "declare module '#imports' {",
646
+ " function getUserSession(event: H3Event): Promise<{ user?: import('#auth-utils').User }>",
647
+ " function setUserSession(event: H3Event, session: { user: import('#auth-utils').User }): Promise<unknown>",
648
+ "}",
649
+ "",
650
+ "export {}",
651
+ ""
652
+ ].join("\n")
653
+ },
654
+ { nuxt: true, nitro: true }
655
+ );
656
+ nuxt.options.css.push(resolver.resolve("./runtime/assets/main.css"));
657
+ addLayout({ src: resolver.resolve("./runtime/app/layouts/cms-admin.vue") }, "cms-admin");
658
+ addComponentsDir({ path: resolver.resolve("./runtime/app/components") });
659
+ addRouteMiddleware({
660
+ name: "cms-auth",
661
+ path: resolver.resolve("./runtime/app/middleware/cms-auth")
662
+ });
663
+ extendPages((pages) => {
664
+ pages.unshift(
665
+ {
666
+ name: "cms-admin",
667
+ path: "/cms",
668
+ file: resolver.resolve("./runtime/app/pages/admin-index.vue")
669
+ },
670
+ {
671
+ name: "cms-admin-login",
672
+ path: "/cms/login",
673
+ file: resolver.resolve("./runtime/app/pages/admin-login.vue")
674
+ },
675
+ {
676
+ name: "cms-admin-media",
677
+ path: "/cms/media",
678
+ file: resolver.resolve("./runtime/app/pages/admin-media.vue")
679
+ },
680
+ {
681
+ name: "cms-admin-collection",
682
+ path: "/cms/:collection()",
683
+ file: resolver.resolve("./runtime/app/pages/admin-collection.vue")
684
+ },
685
+ {
686
+ name: "cms-admin-entry-new",
687
+ path: "/cms/:collection()/new",
688
+ file: resolver.resolve("./runtime/app/pages/admin-entry.vue")
689
+ },
690
+ {
691
+ name: "cms-admin-entry",
692
+ path: "/cms/:collection()/:id()",
693
+ file: resolver.resolve("./runtime/app/pages/admin-entry.vue")
694
+ }
695
+ );
696
+ });
697
+ addServerHandler({
698
+ route: "/api/cms/auth/login",
699
+ method: "post",
700
+ handler: resolver.resolve("./runtime/server/routes/auth/login.post")
701
+ });
702
+ addServerHandler({
703
+ route: "/api/cms/admin/media/presign",
704
+ method: "post",
705
+ handler: resolver.resolve("./runtime/server/api/media-presign.post")
706
+ });
707
+ addServerHandler({
708
+ route: "/api/cms/admin/media",
709
+ method: "get",
710
+ handler: resolver.resolve("./runtime/server/api/media.get")
711
+ });
712
+ addServerHandler({
713
+ route: "/api/cms/admin/media",
714
+ method: "post",
715
+ handler: resolver.resolve("./runtime/server/api/media.post")
716
+ });
717
+ addServerHandler({
718
+ route: "/api/cms/admin/media",
719
+ method: "put",
720
+ handler: resolver.resolve("./runtime/server/api/media.put")
721
+ });
722
+ addServerHandler({
723
+ route: "/api/cms/admin/media",
724
+ method: "delete",
725
+ handler: resolver.resolve("./runtime/server/api/media.delete")
726
+ });
727
+ const api = "/api/cms/admin/:collection";
728
+ addServerHandler({
729
+ route: api,
730
+ method: "get",
731
+ handler: resolver.resolve("./runtime/server/api/collection.get")
732
+ });
733
+ addServerHandler({
734
+ route: api,
735
+ method: "post",
736
+ handler: resolver.resolve("./runtime/server/api/collection.post")
737
+ });
738
+ addServerHandler({
739
+ route: api,
740
+ method: "put",
741
+ handler: resolver.resolve("./runtime/server/api/collection.put")
742
+ });
743
+ addServerHandler({
744
+ route: `${api}/:id`,
745
+ method: "get",
746
+ handler: resolver.resolve("./runtime/server/api/item.get")
747
+ });
748
+ addServerHandler({
749
+ route: `${api}/:id`,
750
+ method: "put",
751
+ handler: resolver.resolve("./runtime/server/api/item.put")
752
+ });
753
+ addServerHandler({
754
+ route: `${api}/:id`,
755
+ method: "delete",
756
+ handler: resolver.resolve("./runtime/server/api/item.delete")
757
+ });
758
+ addServerHandler({
759
+ route: "/api/cms/graphql",
760
+ handler: resolver.resolve("./runtime/server/routes/graphql")
761
+ });
762
+ }
763
+ });
764
+
765
+ export { module$1 as default };