@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,72 @@
1
+ import { GraphQLError, Kind } from "graphql";
2
+ const DEFAULT_MAX_DEPTH = 8;
3
+ const DEFAULT_MAX_FIELDS = 500;
4
+ function measure(selectionSet, fragments, visited) {
5
+ let max = 0;
6
+ for (const selection of selectionSet.selections) {
7
+ if (selection.kind === Kind.FIELD) {
8
+ if (selection.name.value.startsWith("__")) continue;
9
+ const depth = selection.selectionSet ? 1 + measure(selection.selectionSet, fragments, visited) : 1;
10
+ max = Math.max(max, depth);
11
+ } else if (selection.kind === Kind.FRAGMENT_SPREAD) {
12
+ const name = selection.name.value;
13
+ if (visited.has(name)) continue;
14
+ const fragment = fragments.get(name);
15
+ if (fragment) {
16
+ visited.add(name);
17
+ max = Math.max(max, measure(fragment.selectionSet, fragments, visited));
18
+ visited.delete(name);
19
+ }
20
+ } else if (selection.kind === Kind.INLINE_FRAGMENT) {
21
+ max = Math.max(max, measure(selection.selectionSet, fragments, visited));
22
+ }
23
+ }
24
+ return max;
25
+ }
26
+ export function createDepthRule(maxDepth) {
27
+ const limit = typeof maxDepth === "number" && Number.isFinite(maxDepth) && maxDepth > 0 ? maxDepth : DEFAULT_MAX_DEPTH;
28
+ return (context) => ({
29
+ OperationDefinition(node) {
30
+ const fragments = /* @__PURE__ */ new Map();
31
+ for (const definition of context.getDocument().definitions) {
32
+ if (definition.kind === Kind.FRAGMENT_DEFINITION)
33
+ fragments.set(definition.name.value, definition);
34
+ }
35
+ const depth = measure(node.selectionSet, fragments, /* @__PURE__ */ new Set());
36
+ if (depth > limit) {
37
+ context.reportError(
38
+ new GraphQLError(`Query is too deep: depth ${depth} exceeds the maximum of ${limit}`)
39
+ );
40
+ }
41
+ }
42
+ });
43
+ }
44
+ export function createBreadthRule(maxFields) {
45
+ const limit = typeof maxFields === "number" && Number.isFinite(maxFields) && maxFields > 0 ? maxFields : DEFAULT_MAX_FIELDS;
46
+ return (context) => {
47
+ let count = 0;
48
+ let reported = false;
49
+ return {
50
+ Field() {
51
+ count++;
52
+ if (count > limit && !reported) {
53
+ reported = true;
54
+ context.reportError(
55
+ new GraphQLError(
56
+ `Query has too many field selections: exceeds the maximum of ${limit}`
57
+ )
58
+ );
59
+ }
60
+ }
61
+ };
62
+ };
63
+ }
64
+ export function createIntrospectionRule() {
65
+ return (context) => ({
66
+ Field(node) {
67
+ if (node.name.value === "__schema" || node.name.value === "__type") {
68
+ context.reportError(new GraphQLError("Introspection is disabled"));
69
+ }
70
+ }
71
+ });
72
+ }
@@ -0,0 +1,14 @@
1
+ interface Ctx {
2
+ loaders: Map<string, BatchLoader<unknown, unknown>>;
3
+ }
4
+ declare class BatchLoader<K, V> {
5
+ private batchFn;
6
+ private queue;
7
+ private scheduled;
8
+ constructor(batchFn: (keys: K[]) => Promise<Map<K, V>>);
9
+ load(key: K): Promise<V | undefined>;
10
+ private flush;
11
+ }
12
+ export declare function buildCmsSchema(): import("graphql-yoga").GraphQLSchemaWithContext<Ctx & import("graphql-yoga").YogaInitialContext>;
13
+ export declare function createGraphqlContext(): Ctx;
14
+ export {};
@@ -0,0 +1,317 @@
1
+ import {
2
+ and,
3
+ asc,
4
+ desc,
5
+ eq,
6
+ gt,
7
+ gte,
8
+ inArray,
9
+ isNotNull,
10
+ isNull,
11
+ like,
12
+ lt,
13
+ lte,
14
+ ne,
15
+ sql
16
+ } from "drizzle-orm";
17
+ import { GraphQLError, GraphQLScalarType, valueFromASTUntyped } from "graphql";
18
+ import { createSchema } from "graphql-yoga";
19
+ import cmsConfig from "#cms-config";
20
+ import { useDb } from "#cms-db";
21
+ import * as cmsTables from "#cms-tables";
22
+ import { useRuntimeConfig } from "#imports";
23
+ import { mediaPublicUrl, mediaTypeFor, translatableFieldKeys } from "../../shared/index.js";
24
+ import { blockTypeName, blockUnionName, renderGraphqlSdl, typeName } from "../../shared/graphql-sdl.js";
25
+ import { getContentI18n, resolveTable, tableColumns } from "./registry.js";
26
+ const MAX_LIMIT = 100;
27
+ const DEFAULT_LIMIT = 50;
28
+ const LOCALE = Symbol("locale");
29
+ class BatchLoader {
30
+ constructor(batchFn) {
31
+ this.batchFn = batchFn;
32
+ }
33
+ queue = [];
34
+ scheduled = false;
35
+ load(key) {
36
+ return new Promise((resolve, reject) => {
37
+ this.queue.push({ key, resolve, reject });
38
+ if (!this.scheduled) {
39
+ this.scheduled = true;
40
+ queueMicrotask(() => void this.flush());
41
+ }
42
+ });
43
+ }
44
+ async flush() {
45
+ const batch = this.queue;
46
+ this.queue = [];
47
+ this.scheduled = false;
48
+ try {
49
+ const map = await this.batchFn([...new Set(batch.map((b) => b.key))]);
50
+ for (const { key, resolve } of batch) resolve(map.get(key));
51
+ } catch (error) {
52
+ for (const { reject } of batch) reject(error);
53
+ }
54
+ }
55
+ }
56
+ function getLoader(ctx, cacheKey, batchFn) {
57
+ let loader = ctx.loaders.get(cacheKey);
58
+ if (!loader) {
59
+ loader = new BatchLoader(batchFn);
60
+ ctx.loaders.set(cacheKey, loader);
61
+ }
62
+ return loader;
63
+ }
64
+ function tableFor(name) {
65
+ const table = resolveTable(name);
66
+ if (!table) throw new GraphQLError(`No table for collection: ${name}`);
67
+ return table;
68
+ }
69
+ function publishedFilter(entry, table) {
70
+ if (!entry.drafts) return [];
71
+ const status = tableColumns(table).status;
72
+ return status ? [eq(status, "published")] : [];
73
+ }
74
+ function localeOf(parent) {
75
+ return parent[LOCALE] ?? getContentI18n().defaultLocale;
76
+ }
77
+ function resolveLocaleArg(locale) {
78
+ const { locales, defaultLocale } = getContentI18n();
79
+ if (locale == null || locale === "") return defaultLocale;
80
+ if (!locales.includes(String(locale))) {
81
+ throw new GraphQLError(
82
+ `Unknown locale: ${locale}${locales.length ? ` (available: ${locales.join(", ")})` : ""}`
83
+ );
84
+ }
85
+ return String(locale);
86
+ }
87
+ function localizeRow(entry, row, locale) {
88
+ const { defaultLocale } = getContentI18n();
89
+ const result = { ...row, [LOCALE]: locale };
90
+ for (const key of translatableFieldKeys(entry)) {
91
+ const value = row[key];
92
+ result[key] = value?.[locale] ?? value?.[defaultLocale] ?? null;
93
+ }
94
+ return result;
95
+ }
96
+ const OPERATORS = {
97
+ eq: (column, value) => eq(column, value),
98
+ neq: (column, value) => ne(column, value),
99
+ gt: (column, value) => gt(column, value),
100
+ gte: (column, value) => gte(column, value),
101
+ lt: (column, value) => lt(column, value),
102
+ lte: (column, value) => lte(column, value),
103
+ like: (column, value) => like(column, String(value).replaceAll("*", "%")),
104
+ in: (column, value) => inArray(column, value)
105
+ };
106
+ function filterConditions(table, filters) {
107
+ if (!filters) return [];
108
+ const conditions = [];
109
+ for (const [key, ops] of Object.entries(filters)) {
110
+ if (!ops) continue;
111
+ const column = tableColumns(table)[key];
112
+ if (!column) throw new GraphQLError(`Unknown filter field: ${key}`);
113
+ for (const [op, value] of Object.entries(ops)) {
114
+ if (value === void 0) continue;
115
+ if (op === "isNull") {
116
+ conditions.push(value ? isNull(column) : isNotNull(column));
117
+ continue;
118
+ }
119
+ if (value === null) continue;
120
+ if (op === "in" && !value.length) {
121
+ throw new GraphQLError(`Empty in list for ${key}`);
122
+ }
123
+ const operator = OPERATORS[op];
124
+ if (!operator) throw new GraphQLError(`Unknown operator for ${key}: ${op}`);
125
+ conditions.push(operator(column, value));
126
+ }
127
+ }
128
+ return conditions;
129
+ }
130
+ function sortOrder(table, sort) {
131
+ const order = [];
132
+ for (const part of sort ?? []) {
133
+ const column = tableColumns(table)[part.field];
134
+ if (!column) throw new GraphQLError(`Unknown sort field: ${part.field}`);
135
+ order.push(part.direction === "desc" ? desc(column) : asc(column));
136
+ }
137
+ if (!order.length) {
138
+ const fallback = tableColumns(table).createdAt ?? tableColumns(table).id;
139
+ if (fallback) order.push(desc(fallback));
140
+ }
141
+ return order;
142
+ }
143
+ let mediaBase;
144
+ function getMediaBase() {
145
+ mediaBase ??= useRuntimeConfig().public.cms.mediaBaseUrl;
146
+ return mediaBase;
147
+ }
148
+ async function resolveMedia(ctx, key) {
149
+ if (!key) return null;
150
+ const row = await loadMedia(ctx).load(key);
151
+ return row ? mediaObject(key, row) : null;
152
+ }
153
+ function loadById(ctx, targetName) {
154
+ return getLoader(ctx, `byId:${targetName}`, async (ids) => {
155
+ const config = cmsConfig;
156
+ const entry = config[targetName];
157
+ const table = tableFor(targetName);
158
+ const id = tableColumns(table).id;
159
+ const rows = await useDb().select().from(table).where(and(inArray(id, ids), ...publishedFilter(entry, table)));
160
+ return new Map(
161
+ rows.map((row) => [
162
+ row.id,
163
+ row
164
+ ])
165
+ );
166
+ });
167
+ }
168
+ function loadManyToMany(ctx, name, key, field) {
169
+ return getLoader(
170
+ ctx,
171
+ `m2m:${name}:${key}`,
172
+ async (sourceIds) => {
173
+ const config = cmsConfig;
174
+ const targetEntry = config[field.to];
175
+ const join = cmsTables[`${name}_${key}`];
176
+ const target = tableFor(field.to);
177
+ const joinCols = tableColumns(join);
178
+ const rows = await useDb().select({ sourceId: joinCols.sourceId, target }).from(join).innerJoin(target, eq(joinCols.targetId, tableColumns(target).id)).where(
179
+ and(inArray(joinCols.sourceId, sourceIds), ...publishedFilter(targetEntry, target))
180
+ ).orderBy(asc(joinCols.position));
181
+ const map = /* @__PURE__ */ new Map();
182
+ for (const row of rows) {
183
+ if (!map.has(row.sourceId)) map.set(row.sourceId, []);
184
+ map.get(row.sourceId).push(row.target);
185
+ }
186
+ for (const id of sourceIds) {
187
+ if (!map.has(id)) map.set(id, []);
188
+ }
189
+ return map;
190
+ }
191
+ );
192
+ }
193
+ function loadMedia(ctx) {
194
+ return getLoader(ctx, "media", async (keys) => {
195
+ const media = cmsTables.cms_media;
196
+ const keyCol = tableColumns(media).key;
197
+ const rows = await useDb().select().from(media).where(inArray(keyCol, keys));
198
+ return new Map(
199
+ rows.map((row) => [
200
+ row.key,
201
+ row
202
+ ])
203
+ );
204
+ });
205
+ }
206
+ function mediaObject(key, row) {
207
+ return {
208
+ key,
209
+ url: mediaPublicUrl(getMediaBase(), key),
210
+ alt: row?.alt ?? null,
211
+ folder: row?.folder ?? null,
212
+ mime: row?.mime ?? null,
213
+ size: row?.size ?? null,
214
+ width: row?.width ?? null,
215
+ height: row?.height ?? null,
216
+ type: mediaTypeFor(row?.mime, key)
217
+ };
218
+ }
219
+ function entryResolvers(config, name, entry) {
220
+ const resolvers = {};
221
+ for (const [key, field] of Object.entries(entry.fields)) {
222
+ if (field.type === "relation" && field.cardinality === "many-to-many") {
223
+ resolvers[key] = async (parent, _args, ctx) => {
224
+ const rows = await loadManyToMany(ctx, name, key, field).load(parent.id);
225
+ return (rows ?? []).map((row) => localizeRow(config[field.to], row, localeOf(parent)));
226
+ };
227
+ } else if (field.type === "relation") {
228
+ resolvers[key] = async (parent, _args, ctx) => {
229
+ const id = parent[key];
230
+ if (id == null) return null;
231
+ const row = await loadById(ctx, field.to).load(id);
232
+ return row ? localizeRow(config[field.to], row, localeOf(parent)) : null;
233
+ };
234
+ } else if (field.type === "media") {
235
+ resolvers[key] = (parent, _args, ctx) => resolveMedia(ctx, parent[key]);
236
+ }
237
+ }
238
+ return resolvers;
239
+ }
240
+ function blockResolvers(name, key, field) {
241
+ const resolvers = {};
242
+ resolvers[blockUnionName(name, key)] = {
243
+ __resolveType: (value) => blockTypeName(name, key, String(value.type))
244
+ };
245
+ for (const [blockName, block] of Object.entries(field.blocks ?? {})) {
246
+ const mediaFields = Object.entries(block.fields).filter(([, f]) => f.type === "media");
247
+ if (!mediaFields.length) continue;
248
+ const fieldLevel = {};
249
+ for (const [blockFieldKey] of mediaFields) {
250
+ fieldLevel[blockFieldKey] = (parent, _args, ctx) => resolveMedia(ctx, parent[blockFieldKey]);
251
+ }
252
+ resolvers[blockTypeName(name, key, blockName)] = fieldLevel;
253
+ }
254
+ return resolvers;
255
+ }
256
+ export function buildCmsSchema() {
257
+ const config = cmsConfig;
258
+ const entries = Object.entries(config);
259
+ const queryResolvers = {};
260
+ const typeResolvers = {};
261
+ for (const [name, entry] of entries) {
262
+ const gqlType = typeName(name);
263
+ const fieldLevel = entryResolvers(config, name, entry);
264
+ if (Object.keys(fieldLevel).length) typeResolvers[gqlType] = fieldLevel;
265
+ for (const [key, field] of Object.entries(entry.fields)) {
266
+ if (field.type === "blocks") Object.assign(typeResolvers, blockResolvers(name, key, field));
267
+ }
268
+ if (entry.kind === "single") {
269
+ queryResolvers[name] = async (_, args) => {
270
+ const locale = resolveLocaleArg(args.locale);
271
+ const table = tableFor(name);
272
+ const published = publishedFilter(entry, table);
273
+ const [row] = await useDb().select().from(table).where(published.length ? and(...published) : void 0).limit(1);
274
+ return row ? localizeRow(entry, row, locale) : null;
275
+ };
276
+ continue;
277
+ }
278
+ queryResolvers[name] = async (_, args) => {
279
+ const locale = resolveLocaleArg(args.locale);
280
+ const table = tableFor(name);
281
+ const where = [...publishedFilter(entry, table), ...filterConditions(table, args.filters)];
282
+ const limit = Math.min(MAX_LIMIT, Math.max(0, args.limit ?? DEFAULT_LIMIT));
283
+ const offset = Math.max(0, args.offset ?? 0);
284
+ const rows = await useDb().select().from(table).where(where.length ? and(...where) : void 0).orderBy(...sortOrder(table, args.sort)).limit(limit).offset(offset);
285
+ return rows.map((row) => localizeRow(entry, row, locale));
286
+ };
287
+ queryResolvers[`${name}ById`] = async (_, args) => {
288
+ const locale = resolveLocaleArg(args.locale);
289
+ const table = tableFor(name);
290
+ const [row] = await useDb().select().from(table).where(and(eq(tableColumns(table).id, args.id), ...publishedFilter(entry, table))).limit(1);
291
+ return row ? localizeRow(entry, row, locale) : null;
292
+ };
293
+ queryResolvers[`${name}Count`] = async (_, args) => {
294
+ const table = tableFor(name);
295
+ const where = [...publishedFilter(entry, table), ...filterConditions(table, args.filters)];
296
+ const [counted] = await useDb().select({ total: sql`count(*)` }).from(table).where(where.length ? and(...where) : void 0);
297
+ return counted?.total ?? 0;
298
+ };
299
+ }
300
+ const jsonScalar = new GraphQLScalarType({
301
+ name: "JSON",
302
+ serialize: (value) => value,
303
+ parseValue: (value) => value,
304
+ parseLiteral: (ast) => valueFromASTUntyped(ast)
305
+ });
306
+ return createSchema({
307
+ typeDefs: renderGraphqlSdl(config),
308
+ resolvers: {
309
+ JSON: jsonScalar,
310
+ Query: queryResolvers,
311
+ ...typeResolvers
312
+ }
313
+ });
314
+ }
315
+ export function createGraphqlContext() {
316
+ return { loaders: /* @__PURE__ */ new Map() };
317
+ }
@@ -0,0 +1,24 @@
1
+ import type { H3Event } from 'h3';
2
+ import { AwsClient } from 'aws4fetch';
3
+ import type { MediaItem } from '../../shared/index.js';
4
+ interface MediaConfig {
5
+ endpoint: string;
6
+ region: string;
7
+ bucket: string;
8
+ presignExpiry: number;
9
+ accessKeyId: string;
10
+ secretAccessKey: string;
11
+ }
12
+ export declare function encodeKey(key: string): string;
13
+ export declare function useMediaConfig(event: H3Event): {
14
+ media: MediaConfig;
15
+ publicUrl: (key: string) => string | null;
16
+ };
17
+ export declare function useMediaStorage(event: H3Event): {
18
+ media: MediaConfig;
19
+ client: AwsClient;
20
+ bucketUrl: string;
21
+ publicUrl: (key: string) => string | null;
22
+ };
23
+ export declare function toMediaItem(row: Omit<MediaItem, 'type' | 'url'>, publicUrl: (key: string) => string | null): MediaItem;
24
+ export {};
@@ -0,0 +1,38 @@
1
+ import { AwsClient } from "aws4fetch";
2
+ import { createError } from "h3";
3
+ import { useRuntimeConfig } from "#imports";
4
+ import { mediaPublicUrl, mediaTypeFor } from "../../shared/index.js";
5
+ export function encodeKey(key) {
6
+ return key.split("/").map(encodeURIComponent).join("/");
7
+ }
8
+ export function useMediaConfig(event) {
9
+ const config = useRuntimeConfig(event);
10
+ const media = config.cms.media;
11
+ if (!media.endpoint || !media.bucket || !media.accessKeyId || !media.secretAccessKey) {
12
+ throw createError({
13
+ statusCode: 501,
14
+ statusMessage: "Media storage is not configured (cms.media in nuxt.config)"
15
+ });
16
+ }
17
+ const { mediaBaseUrl } = config.public.cms;
18
+ const publicUrl = (key) => mediaPublicUrl(mediaBaseUrl, key);
19
+ return { media, publicUrl };
20
+ }
21
+ export function useMediaStorage(event) {
22
+ const { media, publicUrl } = useMediaConfig(event);
23
+ const client = new AwsClient({
24
+ accessKeyId: media.accessKeyId,
25
+ secretAccessKey: media.secretAccessKey,
26
+ region: media.region,
27
+ service: "s3"
28
+ });
29
+ const bucketUrl = `${media.endpoint.replace(/\/+$/, "")}/${media.bucket}`;
30
+ return { media, client, bucketUrl, publicUrl };
31
+ }
32
+ export function toMediaItem(row, publicUrl) {
33
+ return {
34
+ ...row,
35
+ type: mediaTypeFor(row.mime, row.key),
36
+ url: publicUrl(row.key)
37
+ };
38
+ }
@@ -0,0 +1,20 @@
1
+ import type { AnySQLiteColumn, SQLiteTable } from 'drizzle-orm/sqlite-core';
2
+ import type { H3Event } from 'h3';
3
+ import { z } from 'zod';
4
+ import type { CmsEntry, CmsI18n } from '../../shared/index.js';
5
+ export declare function getContentI18n(): CmsI18n;
6
+ export declare function resolveTable(name: string): SQLiteTable | undefined;
7
+ export declare function tableColumns(table: SQLiteTable): Record<string, AnySQLiteColumn>;
8
+ export declare function idColumn(table: SQLiteTable): AnySQLiteColumn;
9
+ export declare function withUpdatedAt(table: SQLiteTable, values: Record<string, unknown>): {
10
+ [x: string]: unknown;
11
+ };
12
+ export declare function getRegistryEntry(event: H3Event): {
13
+ name: string;
14
+ entry: CmsEntry;
15
+ table: SQLiteTable;
16
+ };
17
+ export declare function buildValidator(entry: CmsEntry): z.ZodObject<{
18
+ [x: string]: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
19
+ }, z.core.$strip>;
20
+ export declare function parseId(event: H3Event): string;
@@ -0,0 +1,47 @@
1
+ import { createError, getRouterParam } from "h3";
2
+ import { z } from "zod";
3
+ import cmsConfig from "#cms-config";
4
+ import * as cmsTables from "#cms-tables";
5
+ import { useRuntimeConfig } from "#imports";
6
+ import { buildEntrySchema } from "../../shared/validation.js";
7
+ let contentI18n;
8
+ export function getContentI18n() {
9
+ if (!contentI18n) {
10
+ const { i18n } = useRuntimeConfig().public.cms ?? {};
11
+ contentI18n = { locales: i18n?.locales ?? [], defaultLocale: i18n?.defaultLocale ?? "en" };
12
+ }
13
+ return contentI18n;
14
+ }
15
+ export function resolveTable(name) {
16
+ const entry = cmsConfig[name];
17
+ return entry?.table ?? cmsTables[name];
18
+ }
19
+ export function tableColumns(table) {
20
+ return table;
21
+ }
22
+ export function idColumn(table) {
23
+ return tableColumns(table).id;
24
+ }
25
+ export function withUpdatedAt(table, values) {
26
+ const set = { ...values };
27
+ if ("updatedAt" in table) set.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
28
+ return set;
29
+ }
30
+ export function getRegistryEntry(event) {
31
+ const name = getRouterParam(event, "collection");
32
+ const config = cmsConfig;
33
+ if (!name || !Object.hasOwn(config, name)) {
34
+ throw createError({ statusCode: 404, statusMessage: `Unknown collection: ${name}` });
35
+ }
36
+ const table = resolveTable(name);
37
+ if (!table) {
38
+ throw createError({ statusCode: 500, statusMessage: `No table for collection: ${name}` });
39
+ }
40
+ return { name, entry: config[name], table };
41
+ }
42
+ export function buildValidator(entry) {
43
+ return buildEntrySchema(entry, getContentI18n());
44
+ }
45
+ export function parseId(event) {
46
+ return z.string().min(1).parse(getRouterParam(event, "id"));
47
+ }
@@ -0,0 +1,12 @@
1
+ import type { CmsEntry } from '../../shared/index.js';
2
+ type Row = Record<string, unknown>;
3
+ export declare function splitRelationValues(entry: CmsEntry, body: Row): {
4
+ values: {
5
+ [k: string]: unknown;
6
+ };
7
+ lists: Record<string, string[]>;
8
+ };
9
+ export declare function assertRelationTargets(entry: CmsEntry, lists: Record<string, string[]>): Promise<void>;
10
+ export declare function saveManyToMany(name: string, sourceId: string, lists: Record<string, string[]>): Promise<void>;
11
+ export declare function attachManyToMany<T extends Row>(name: string, entry: CmsEntry, rows: T[]): Promise<T[]>;
12
+ export {};
@@ -0,0 +1,80 @@
1
+ import { asc, eq, inArray } from "drizzle-orm";
2
+ import { createError } from "h3";
3
+ import { useDb } from "#cms-db";
4
+ import * as cmsTables from "#cms-tables";
5
+ import { resolveTable, tableColumns } from "./registry.js";
6
+ function manyToManyKeys(entry) {
7
+ return Object.entries(entry.fields).filter(([, field]) => field.type === "relation" && field.cardinality === "many-to-many").map(([key]) => key);
8
+ }
9
+ function joinTable(name, key) {
10
+ const table = cmsTables[`${name}_${key}`];
11
+ if (!table) {
12
+ throw createError({ statusCode: 500, statusMessage: `No join table for ${name}.${key}` });
13
+ }
14
+ const columns = table;
15
+ return {
16
+ table,
17
+ sourceId: columns.sourceId,
18
+ targetId: columns.targetId,
19
+ position: columns.position
20
+ };
21
+ }
22
+ export function splitRelationValues(entry, body) {
23
+ const keys = manyToManyKeys(entry);
24
+ const values = Object.fromEntries(Object.entries(body).filter(([key]) => !keys.includes(key)));
25
+ const lists = {};
26
+ for (const key of keys) {
27
+ const ids = body[key] ?? [];
28
+ lists[key] = [...new Set(ids)];
29
+ }
30
+ return { values, lists };
31
+ }
32
+ export async function assertRelationTargets(entry, lists) {
33
+ const db = useDb();
34
+ for (const [key, ids] of Object.entries(lists)) {
35
+ if (!ids.length) continue;
36
+ const field = entry.fields[key];
37
+ const target = resolveTable(field.to);
38
+ if (!target) continue;
39
+ const idCol = tableColumns(target).id;
40
+ const rows = await db.select({ id: idCol }).from(target).where(inArray(idCol, ids));
41
+ if (rows.length !== ids.length) {
42
+ const found = new Set(rows.map((row) => row.id));
43
+ const missing = ids.filter((id) => !found.has(id));
44
+ throw createError({
45
+ statusCode: 422,
46
+ statusMessage: `Unknown ${field.to} id(s) for '${key}': ${missing.join(", ")}`
47
+ });
48
+ }
49
+ }
50
+ }
51
+ export async function saveManyToMany(name, sourceId, lists) {
52
+ const db = useDb();
53
+ for (const [key, ids] of Object.entries(lists)) {
54
+ const join = joinTable(name, key);
55
+ await db.delete(join.table).where(eq(join.sourceId, sourceId));
56
+ if (ids.length) {
57
+ await db.insert(join.table).values(ids.map((targetId, position) => ({ sourceId, targetId, position })));
58
+ }
59
+ }
60
+ }
61
+ export async function attachManyToMany(name, entry, rows) {
62
+ const keys = manyToManyKeys(entry);
63
+ const ids = rows.map((row) => row.id).filter((id) => id != null);
64
+ if (!keys.length || !ids.length) return rows;
65
+ const db = useDb();
66
+ for (const key of keys) {
67
+ const join = joinTable(name, key);
68
+ const links = await db.select().from(join.table).where(inArray(join.sourceId, ids)).orderBy(asc(join.position));
69
+ const grouped = /* @__PURE__ */ new Map();
70
+ for (const link of links) {
71
+ const list = grouped.get(link.sourceId) ?? [];
72
+ list.push(link.targetId);
73
+ grouped.set(link.sourceId, list);
74
+ }
75
+ for (const row of rows) {
76
+ row[key] = grouped.get(row.id) ?? [];
77
+ }
78
+ }
79
+ return rows;
80
+ }
@@ -0,0 +1,2 @@
1
+ import type { H3Event } from 'h3';
2
+ export declare function requireAdmin(event: H3Event): Promise<void>;
@@ -0,0 +1,13 @@
1
+ import { createError } from "h3";
2
+ import { getUserSession, useRuntimeConfig } from "#imports";
3
+ export async function requireAdmin(event) {
4
+ const session = await getUserSession(event);
5
+ const email = session.user?.email;
6
+ if (!email) {
7
+ throw createError({ statusCode: 401, statusMessage: "Authentication required" });
8
+ }
9
+ const { adminEmail } = useRuntimeConfig(event).cms;
10
+ if (!adminEmail || email.toLowerCase() !== adminEmail.toLowerCase()) {
11
+ throw createError({ statusCode: 403, statusMessage: "Not authorized" });
12
+ }
13
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: {};
2
+ export default _default;
@@ -0,0 +1 @@
1
+ export default {};
@@ -0,0 +1,5 @@
1
+ import type { CmsConfig } from './index.js';
2
+ export declare function typeName(name: string): string;
3
+ export declare function blockUnionName(entryName: string, fieldKey: string): string;
4
+ export declare function blockTypeName(entryName: string, fieldKey: string, blockName: string): string;
5
+ export declare function renderGraphqlSdl(config: CmsConfig): string;