@aphexcms/cms-core 0.1.1 → 0.1.3
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.
- package/package.json +22 -5
- package/src/api/assets.ts +0 -75
- package/src/api/client.ts +0 -150
- package/src/api/documents.ts +0 -102
- package/src/api/index.ts +0 -7
- package/src/api/organizations.ts +0 -154
- package/src/api/types.ts +0 -34
- package/src/app.d.ts +0 -19
- package/src/auth/MULTI_TENANCY_PLAN.md +0 -1183
- package/src/auth/auth-errors.ts +0 -23
- package/src/auth/auth-hooks.ts +0 -132
- package/src/auth/provider.ts +0 -25
- package/src/client/index.ts +0 -47
- package/src/components/AdminApp.svelte +0 -1078
- package/src/components/admin/AdminLayout.svelte +0 -115
- package/src/components/admin/DocumentEditor.svelte +0 -795
- package/src/components/admin/DocumentTypesList.svelte +0 -97
- package/src/components/admin/ObjectModal.svelte +0 -135
- package/src/components/admin/SchemaField.svelte +0 -171
- package/src/components/admin/fields/ArrayField.svelte +0 -266
- package/src/components/admin/fields/BooleanField.svelte +0 -35
- package/src/components/admin/fields/ImageField.svelte +0 -284
- package/src/components/admin/fields/NumberField.svelte +0 -82
- package/src/components/admin/fields/ReferenceField.svelte +0 -260
- package/src/components/admin/fields/SlugField.svelte +0 -74
- package/src/components/admin/fields/StringField.svelte +0 -40
- package/src/components/admin/fields/TextareaField.svelte +0 -40
- package/src/components/fields/index.ts +0 -9
- package/src/components/index.ts +0 -16
- package/src/components/layout/OrganizationSwitcher.svelte +0 -218
- package/src/components/layout/Sidebar.svelte +0 -88
- package/src/components/layout/sidebar/AppSidebar.svelte +0 -63
- package/src/components/layout/sidebar/NavMain.svelte +0 -95
- package/src/components/layout/sidebar/NavSecondary.svelte +0 -69
- package/src/components/layout/sidebar/NavUser.svelte +0 -85
- package/src/config.ts +0 -18
- package/src/db/adapters/index.ts +0 -3
- package/src/db/index.ts +0 -5
- package/src/db/interfaces/asset.ts +0 -61
- package/src/db/interfaces/document.ts +0 -53
- package/src/db/interfaces/index.ts +0 -98
- package/src/db/interfaces/organization.ts +0 -51
- package/src/db/interfaces/schema.ts +0 -13
- package/src/db/interfaces/user.ts +0 -16
- package/src/db/utils/reference-resolver.ts +0 -119
- package/src/define.ts +0 -7
- package/src/email/index.ts +0 -5
- package/src/email/interfaces/email.ts +0 -45
- package/src/engine.ts +0 -85
- package/src/field-validation/rule.ts +0 -287
- package/src/field-validation/utils.ts +0 -91
- package/src/hooks.ts +0 -142
- package/src/index.ts +0 -5
- package/src/lib/is-mobile.svelte.ts +0 -9
- package/src/lib/utils.ts +0 -13
- package/src/plugins/README.md +0 -154
- package/src/routes/assets-by-id.ts +0 -161
- package/src/routes/assets-cdn.ts +0 -185
- package/src/routes/assets.ts +0 -116
- package/src/routes/documents-by-id.ts +0 -188
- package/src/routes/documents-publish.ts +0 -211
- package/src/routes/documents.ts +0 -172
- package/src/routes/index.ts +0 -13
- package/src/routes/organizations-by-id.ts +0 -258
- package/src/routes/organizations-invitations.ts +0 -183
- package/src/routes/organizations-members.ts +0 -301
- package/src/routes/organizations-switch.ts +0 -74
- package/src/routes/organizations.ts +0 -146
- package/src/routes/schemas-by-type.ts +0 -35
- package/src/routes/schemas.ts +0 -19
- package/src/routes-exports.ts +0 -42
- package/src/schema-context.svelte.ts +0 -24
- package/src/schema-utils/cleanup.ts +0 -116
- package/src/schema-utils/index.ts +0 -4
- package/src/schema-utils/utils.ts +0 -47
- package/src/schema-utils/validator.ts +0 -58
- package/src/server/index.ts +0 -40
- package/src/services/asset-service.ts +0 -256
- package/src/services/index.ts +0 -6
- package/src/storage/adapters/index.ts +0 -2
- package/src/storage/adapters/local-storage-adapter.ts +0 -215
- package/src/storage/index.ts +0 -8
- package/src/storage/interfaces/index.ts +0 -2
- package/src/storage/interfaces/storage.ts +0 -114
- package/src/storage/providers/storage.ts +0 -83
- package/src/types/asset.ts +0 -81
- package/src/types/auth.ts +0 -80
- package/src/types/config.ts +0 -45
- package/src/types/document.ts +0 -38
- package/src/types/index.ts +0 -8
- package/src/types/organization.ts +0 -119
- package/src/types/schemas.ts +0 -151
- package/src/types/sidebar.ts +0 -37
- package/src/types/user.ts +0 -17
- package/src/utils/content-hash.ts +0 -75
- package/src/utils/image-url.ts +0 -204
- package/src/utils/index.ts +0 -12
- package/src/utils/slug.ts +0 -33
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
// Sanity-style validation Rule implementation
|
|
2
|
-
export interface ValidationMarker {
|
|
3
|
-
level: 'error' | 'warning' | 'info';
|
|
4
|
-
message: string;
|
|
5
|
-
path?: string[];
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface ValidationContext {
|
|
9
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
-
document?: any;
|
|
11
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
-
parent?: any;
|
|
13
|
-
path?: string[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface CustomValidator<T = unknown> {
|
|
17
|
-
(
|
|
18
|
-
value: T,
|
|
19
|
-
context: ValidationContext
|
|
20
|
-
): ValidationMarker[] | string | boolean | Promise<ValidationMarker[] | string | boolean>;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface FieldReference {
|
|
24
|
-
__fieldReference: true;
|
|
25
|
-
path: string | string[];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export class Rule {
|
|
29
|
-
private _required: boolean = false;
|
|
30
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
|
-
private _rules: Array<{ type: string; constraint?: any; message?: string }> = [];
|
|
32
|
-
private _level: 'error' | 'warning' | 'info' = 'error';
|
|
33
|
-
private _message?: string;
|
|
34
|
-
|
|
35
|
-
static FIELD_REF = Symbol('fieldReference');
|
|
36
|
-
|
|
37
|
-
static valueOfField(path: string | string[]): FieldReference {
|
|
38
|
-
return {
|
|
39
|
-
__fieldReference: true,
|
|
40
|
-
path
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
valueOfField(path: string | string[]): FieldReference {
|
|
45
|
-
return Rule.valueOfField(path);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
required(): Rule {
|
|
49
|
-
const newRule = this.clone();
|
|
50
|
-
newRule._required = true;
|
|
51
|
-
return newRule;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
optional(): Rule {
|
|
55
|
-
const newRule = this.clone();
|
|
56
|
-
newRule._required = false;
|
|
57
|
-
return newRule;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
min(len: number | string | FieldReference): Rule {
|
|
61
|
-
const newRule = this.clone();
|
|
62
|
-
newRule._rules.push({ type: 'min', constraint: len });
|
|
63
|
-
return newRule;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
max(len: number | string | FieldReference): Rule {
|
|
67
|
-
const newRule = this.clone();
|
|
68
|
-
newRule._rules.push({ type: 'max', constraint: len });
|
|
69
|
-
return newRule;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
length(len: number | FieldReference): Rule {
|
|
73
|
-
const newRule = this.clone();
|
|
74
|
-
newRule._rules.push({ type: 'length', constraint: len });
|
|
75
|
-
return newRule;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
email(): Rule {
|
|
79
|
-
const newRule = this.clone();
|
|
80
|
-
newRule._rules.push({ type: 'email' });
|
|
81
|
-
return newRule;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
uri(options?: { scheme?: RegExp[]; allowRelative?: boolean }): Rule {
|
|
85
|
-
const newRule = this.clone();
|
|
86
|
-
newRule._rules.push({ type: 'uri', constraint: options });
|
|
87
|
-
return newRule;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
regex(pattern: RegExp, name?: string): Rule {
|
|
91
|
-
const newRule = this.clone();
|
|
92
|
-
newRule._rules.push({ type: 'regex', constraint: { pattern, name } });
|
|
93
|
-
return newRule;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
positive(): Rule {
|
|
97
|
-
const newRule = this.clone();
|
|
98
|
-
newRule._rules.push({ type: 'positive' });
|
|
99
|
-
return newRule;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
negative(): Rule {
|
|
103
|
-
const newRule = this.clone();
|
|
104
|
-
newRule._rules.push({ type: 'negative' });
|
|
105
|
-
return newRule;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
integer(): Rule {
|
|
109
|
-
const newRule = this.clone();
|
|
110
|
-
newRule._rules.push({ type: 'integer' });
|
|
111
|
-
return newRule;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
greaterThan(num: number | FieldReference): Rule {
|
|
115
|
-
const newRule = this.clone();
|
|
116
|
-
newRule._rules.push({ type: 'greaterThan', constraint: num });
|
|
117
|
-
return newRule;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
lessThan(num: number | FieldReference): Rule {
|
|
121
|
-
const newRule = this.clone();
|
|
122
|
-
newRule._rules.push({ type: 'lessThan', constraint: num });
|
|
123
|
-
return newRule;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
custom<T = unknown>(fn: CustomValidator<T>): Rule {
|
|
127
|
-
const newRule = this.clone();
|
|
128
|
-
newRule._rules.push({ type: 'custom', constraint: fn });
|
|
129
|
-
return newRule;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
error(message?: string): Rule {
|
|
133
|
-
const newRule = this.clone();
|
|
134
|
-
newRule._level = 'error';
|
|
135
|
-
newRule._message = message;
|
|
136
|
-
return newRule;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
warning(message?: string): Rule {
|
|
140
|
-
const newRule = this.clone();
|
|
141
|
-
newRule._level = 'warning';
|
|
142
|
-
newRule._message = message;
|
|
143
|
-
return newRule;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
info(message?: string): Rule {
|
|
147
|
-
const newRule = this.clone();
|
|
148
|
-
newRule._level = 'info';
|
|
149
|
-
newRule._message = message;
|
|
150
|
-
return newRule;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
clone(): Rule {
|
|
154
|
-
const newRule = new Rule();
|
|
155
|
-
newRule._required = this._required;
|
|
156
|
-
newRule._rules = [...this._rules];
|
|
157
|
-
newRule._level = this._level;
|
|
158
|
-
newRule._message = this._message;
|
|
159
|
-
return newRule;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async validate(value: unknown, context: ValidationContext = {}): Promise<ValidationMarker[]> {
|
|
163
|
-
const markers: ValidationMarker[] = [];
|
|
164
|
-
|
|
165
|
-
// Check required
|
|
166
|
-
if (this._required && (value === undefined || value === null || value === '')) {
|
|
167
|
-
markers.push({
|
|
168
|
-
level: this._level,
|
|
169
|
-
message: this._message || 'Required',
|
|
170
|
-
path: context.path
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// If value is empty and not required, skip other validations
|
|
175
|
-
if (!this._required && (value === undefined || value === null || value === '')) {
|
|
176
|
-
return markers;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Run other validations
|
|
180
|
-
for (const rule of this._rules) {
|
|
181
|
-
try {
|
|
182
|
-
const result = await this.validateRule(rule, value, context);
|
|
183
|
-
if (result) {
|
|
184
|
-
markers.push({
|
|
185
|
-
level: this._level,
|
|
186
|
-
message: this._message || result,
|
|
187
|
-
path: context.path
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
} catch (error) {
|
|
191
|
-
markers.push({
|
|
192
|
-
level: 'error',
|
|
193
|
-
message: `Validation error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
194
|
-
path: context.path
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return markers;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
private async validateRule(
|
|
203
|
-
rule: { type: string; constraint?: any },
|
|
204
|
-
value: unknown,
|
|
205
|
-
context: ValidationContext
|
|
206
|
-
): Promise<string | null> {
|
|
207
|
-
switch (rule.type) {
|
|
208
|
-
case 'min':
|
|
209
|
-
if (typeof value === 'string' && value.length < rule.constraint) {
|
|
210
|
-
return `Must be at least ${rule.constraint} characters`;
|
|
211
|
-
}
|
|
212
|
-
if (typeof value === 'number' && value < rule.constraint) {
|
|
213
|
-
return `Must be at least ${rule.constraint}`;
|
|
214
|
-
}
|
|
215
|
-
break;
|
|
216
|
-
|
|
217
|
-
case 'max':
|
|
218
|
-
if (typeof value === 'string' && value.length > rule.constraint) {
|
|
219
|
-
return `Must be at most ${rule.constraint} characters`;
|
|
220
|
-
}
|
|
221
|
-
if (typeof value === 'number' && value > rule.constraint) {
|
|
222
|
-
return `Must be at most ${rule.constraint}`;
|
|
223
|
-
}
|
|
224
|
-
break;
|
|
225
|
-
|
|
226
|
-
case 'email':
|
|
227
|
-
if (typeof value === 'string' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
228
|
-
return 'Must be a valid email address';
|
|
229
|
-
}
|
|
230
|
-
break;
|
|
231
|
-
|
|
232
|
-
case 'uri':
|
|
233
|
-
if (typeof value === 'string') {
|
|
234
|
-
try {
|
|
235
|
-
new URL(value);
|
|
236
|
-
} catch {
|
|
237
|
-
return 'Must be a valid URL';
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
break;
|
|
241
|
-
|
|
242
|
-
case 'regex':
|
|
243
|
-
if (typeof value === 'string' && !rule.constraint.pattern.test(value)) {
|
|
244
|
-
return `Must match pattern${rule.constraint.name ? ` (${rule.constraint.name})` : ''}`;
|
|
245
|
-
}
|
|
246
|
-
break;
|
|
247
|
-
|
|
248
|
-
case 'positive':
|
|
249
|
-
if (typeof value === 'number' && value <= 0) {
|
|
250
|
-
return 'Must be positive';
|
|
251
|
-
}
|
|
252
|
-
break;
|
|
253
|
-
|
|
254
|
-
case 'negative':
|
|
255
|
-
if (typeof value === 'number' && value >= 0) {
|
|
256
|
-
return 'Must be negative';
|
|
257
|
-
}
|
|
258
|
-
break;
|
|
259
|
-
|
|
260
|
-
case 'integer':
|
|
261
|
-
if (typeof value === 'number' && !Number.isInteger(value)) {
|
|
262
|
-
return 'Must be an integer';
|
|
263
|
-
}
|
|
264
|
-
break;
|
|
265
|
-
|
|
266
|
-
case 'custom': {
|
|
267
|
-
const customResult = await rule.constraint(value, context);
|
|
268
|
-
if (customResult === false) {
|
|
269
|
-
return 'Validation failed';
|
|
270
|
-
}
|
|
271
|
-
if (typeof customResult === 'string') {
|
|
272
|
-
return customResult;
|
|
273
|
-
}
|
|
274
|
-
if (Array.isArray(customResult) && customResult.length > 0) {
|
|
275
|
-
return customResult[0].message;
|
|
276
|
-
}
|
|
277
|
-
break;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
return null;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
isRequired(): boolean {
|
|
285
|
-
return this._required;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import type { Field } from '../types';
|
|
2
|
-
import { Rule } from './rule.js';
|
|
3
|
-
|
|
4
|
-
export interface ValidationError {
|
|
5
|
-
level: 'error' | 'warning' | 'info';
|
|
6
|
-
message: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Check if a field is required based on its validation rules
|
|
11
|
-
*/
|
|
12
|
-
export function isFieldRequired(field: Field): boolean {
|
|
13
|
-
if (!field.validation) return false;
|
|
14
|
-
try {
|
|
15
|
-
const validationFn = Array.isArray(field.validation) ? field.validation[0] : field.validation;
|
|
16
|
-
if (!validationFn) return false;
|
|
17
|
-
const rule = validationFn(new Rule());
|
|
18
|
-
return rule.isRequired();
|
|
19
|
-
} catch {
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Validate a field value against its validation rules
|
|
26
|
-
*/
|
|
27
|
-
export async function validateField(
|
|
28
|
-
field: Field,
|
|
29
|
-
value: any,
|
|
30
|
-
context: any = {}
|
|
31
|
-
): Promise<{
|
|
32
|
-
isValid: boolean;
|
|
33
|
-
errors: ValidationError[];
|
|
34
|
-
}> {
|
|
35
|
-
if (!field.validation) {
|
|
36
|
-
return { isValid: true, errors: [] };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
const validationFunctions = Array.isArray(field.validation)
|
|
41
|
-
? field.validation
|
|
42
|
-
: [field.validation];
|
|
43
|
-
|
|
44
|
-
const allErrors: ValidationError[] = [];
|
|
45
|
-
|
|
46
|
-
for (const validationFn of validationFunctions) {
|
|
47
|
-
const rule = validationFn(new Rule());
|
|
48
|
-
|
|
49
|
-
if (!(rule instanceof Rule)) {
|
|
50
|
-
console.error(
|
|
51
|
-
`Validation function for field "${field.name}" did not return a Rule object. Make sure you are chaining validation methods and returning the result.`
|
|
52
|
-
);
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const markers = await rule.validate(value, {
|
|
57
|
-
path: [field.name],
|
|
58
|
-
...context
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
allErrors.push(
|
|
62
|
-
...markers.map((marker) => ({
|
|
63
|
-
level: marker.level,
|
|
64
|
-
message: marker.message
|
|
65
|
-
}))
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const isValid = allErrors.filter((e) => e.level === 'error').length === 0;
|
|
70
|
-
|
|
71
|
-
return { isValid, errors: allErrors };
|
|
72
|
-
} catch (error) {
|
|
73
|
-
console.error('Validation error:', error);
|
|
74
|
-
return {
|
|
75
|
-
isValid: false,
|
|
76
|
-
errors: [{ level: 'error', message: 'Validation failed' }]
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Get validation CSS classes for input styling
|
|
83
|
-
*/
|
|
84
|
-
export function getValidationClasses(hasErrors: boolean): string {
|
|
85
|
-
if (hasErrors) {
|
|
86
|
-
return 'border-destructive border-2';
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// No green styling for success - only show red for errors
|
|
90
|
-
return '';
|
|
91
|
-
}
|
package/src/hooks.ts
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import type { Handle } from '@sveltejs/kit';
|
|
2
|
-
import type { CMSConfig } from './types/index.js';
|
|
3
|
-
import type { DatabaseAdapter } from './db/index.js';
|
|
4
|
-
import type { AssetService } from './services/asset-service.js';
|
|
5
|
-
import type { StorageAdapter } from './storage/interfaces/storage.js';
|
|
6
|
-
import type { EmailAdapter } from './email/index.js';
|
|
7
|
-
import type { AuthProvider } from './auth/provider.js';
|
|
8
|
-
import { handleAuthHook } from './auth/auth-hooks.js';
|
|
9
|
-
import { createStorageAdapter as createStorageAdapterProvider } from './storage/providers/storage.js';
|
|
10
|
-
import { AssetService as AssetServiceClass } from './services/asset-service.js';
|
|
11
|
-
import { createCMS, CMSEngine } from './engine.js';
|
|
12
|
-
|
|
13
|
-
// Singleton instances - created once per application lifecycle
|
|
14
|
-
export interface CMSInstances {
|
|
15
|
-
config: CMSConfig;
|
|
16
|
-
assetService: AssetService;
|
|
17
|
-
storageAdapter: StorageAdapter;
|
|
18
|
-
databaseAdapter: DatabaseAdapter;
|
|
19
|
-
emailAdapter?: EmailAdapter | null;
|
|
20
|
-
cmsEngine: CMSEngine;
|
|
21
|
-
auth?: AuthProvider;
|
|
22
|
-
pluginRoutes?: Map<
|
|
23
|
-
string,
|
|
24
|
-
{ handler: (event: any) => Promise<Response> | Response; pluginName: string }
|
|
25
|
-
>;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
let cmsInstances: CMSInstances | null = null;
|
|
29
|
-
let lastConfigHash: string | null = null;
|
|
30
|
-
|
|
31
|
-
// Helper to generate a simple hash of schema types for change detection
|
|
32
|
-
function getConfigHash(config: CMSConfig): string {
|
|
33
|
-
const schemaNames = config.schemaTypes
|
|
34
|
-
.map((s) => `${s.name}:${s.fields.length}:${JSON.stringify(s.fields)}`)
|
|
35
|
-
.join(',');
|
|
36
|
-
return schemaNames;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Factory function to create the default local storage adapter
|
|
40
|
-
function createDefaultStorageAdapter(): StorageAdapter {
|
|
41
|
-
return createStorageAdapterProvider('local', {
|
|
42
|
-
basePath: './storage/assets', // Private storage - not in static/, not served in production
|
|
43
|
-
baseUrl: '' // No direct URL - all access through /assets/{id}/{filename}
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function createCMSHook(config: CMSConfig): Handle {
|
|
48
|
-
return async ({ event, resolve }) => {
|
|
49
|
-
// Note: In dev mode, /storage/ might be accessible via Vite dev server
|
|
50
|
-
// In production, only /static/ folder is served - /storage/ is private
|
|
51
|
-
|
|
52
|
-
const currentConfigHash = getConfigHash(config);
|
|
53
|
-
const configChanged = lastConfigHash !== null && currentConfigHash !== lastConfigHash;
|
|
54
|
-
|
|
55
|
-
// Initialize CMS instances once at application startup
|
|
56
|
-
if (!cmsInstances) {
|
|
57
|
-
console.log('🚀 Initializing CMS...');
|
|
58
|
-
const databaseAdapter = config.database;
|
|
59
|
-
// Use the storage adapter from config, or create the default local one.
|
|
60
|
-
const storageAdapter = config.storage ?? createDefaultStorageAdapter();
|
|
61
|
-
const emailAdapter = config.email ?? null;
|
|
62
|
-
const assetService = new AssetServiceClass(storageAdapter, databaseAdapter);
|
|
63
|
-
const cmsEngine = createCMS(config, databaseAdapter);
|
|
64
|
-
|
|
65
|
-
await cmsEngine.initialize();
|
|
66
|
-
|
|
67
|
-
// Build plugin route map (do this ONCE at startup)
|
|
68
|
-
const pluginRoutes = new Map<
|
|
69
|
-
string,
|
|
70
|
-
{ handler: (event: any) => Promise<Response> | Response; pluginName: string }
|
|
71
|
-
>();
|
|
72
|
-
if (config.plugins && config.plugins.length > 0) {
|
|
73
|
-
for (const plugin of config.plugins) {
|
|
74
|
-
if (plugin.routes) {
|
|
75
|
-
for (const [path, handler] of Object.entries(plugin.routes)) {
|
|
76
|
-
pluginRoutes.set(path, { handler, pluginName: plugin.name });
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
cmsInstances = {
|
|
83
|
-
config,
|
|
84
|
-
databaseAdapter: databaseAdapter,
|
|
85
|
-
assetService: assetService,
|
|
86
|
-
storageAdapter: storageAdapter,
|
|
87
|
-
emailAdapter: emailAdapter,
|
|
88
|
-
cmsEngine: cmsEngine,
|
|
89
|
-
auth: config.auth?.provider,
|
|
90
|
-
pluginRoutes
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
// Install plugins
|
|
94
|
-
if (config.plugins && config.plugins.length > 0) {
|
|
95
|
-
for (const plugin of config.plugins) {
|
|
96
|
-
console.log(`🔌 Installing plugin: ${plugin.name}`);
|
|
97
|
-
await plugin.install(cmsInstances);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
lastConfigHash = currentConfigHash;
|
|
102
|
-
} else if (configChanged) {
|
|
103
|
-
// HMR: Config changed, re-sync schemas
|
|
104
|
-
console.log('🔄 Schema types changed, re-syncing...');
|
|
105
|
-
console.log('Old hash:', lastConfigHash?.substring(0, 100) + '...');
|
|
106
|
-
console.log('New hash:', currentConfigHash.substring(0, 100) + '...');
|
|
107
|
-
console.log(
|
|
108
|
-
'Schema types being updated:',
|
|
109
|
-
config.schemaTypes.map((s) => s.name)
|
|
110
|
-
);
|
|
111
|
-
cmsInstances.cmsEngine.updateConfig(config);
|
|
112
|
-
await cmsInstances.cmsEngine.initialize();
|
|
113
|
-
lastConfigHash = currentConfigHash;
|
|
114
|
-
console.log('✅ Schema sync complete');
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Inject shared CMS services into locals (reuse singleton instances)
|
|
118
|
-
event.locals.aphexCMS = cmsInstances;
|
|
119
|
-
|
|
120
|
-
// Auth protection if configured
|
|
121
|
-
if (cmsInstances.auth) {
|
|
122
|
-
const authResponse = await handleAuthHook(
|
|
123
|
-
event,
|
|
124
|
-
config,
|
|
125
|
-
cmsInstances.auth,
|
|
126
|
-
cmsInstances.databaseAdapter
|
|
127
|
-
);
|
|
128
|
-
if (authResponse) return authResponse;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Check if a plugin handles this route (O(1) lookup)
|
|
132
|
-
if (cmsInstances.pluginRoutes && cmsInstances.pluginRoutes.size > 0) {
|
|
133
|
-
const pluginRoute = cmsInstances.pluginRoutes.get(event.url.pathname);
|
|
134
|
-
if (pluginRoute) {
|
|
135
|
-
console.log(`🔌 Plugin ${pluginRoute.pluginName} handling route: ${event.url.pathname}`);
|
|
136
|
-
return pluginRoute.handler(event);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return resolve(event);
|
|
141
|
-
};
|
|
142
|
-
}
|
package/src/index.ts
DELETED
package/src/lib/utils.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { clsx, type ClassValue } from 'clsx';
|
|
2
|
-
import { twMerge } from 'tailwind-merge';
|
|
3
|
-
|
|
4
|
-
export function cn(...inputs: ClassValue[]) {
|
|
5
|
-
return twMerge(clsx(inputs));
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
-
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, 'child'> : T;
|
|
10
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
-
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, 'children'> : T;
|
|
12
|
-
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
|
13
|
-
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|
package/src/plugins/README.md
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
# Plugin Architecture: A Sanity-like "Parts" System
|
|
2
|
-
|
|
3
|
-
This document outlines a vision for a highly extensible, decoupled plugin architecture for Aphex CMS, inspired by the "parts" system used in Sanity Studio.
|
|
4
|
-
|
|
5
|
-
## Core Concept: The "Parts" System
|
|
6
|
-
|
|
7
|
-
The goal is to move away from a specialized plugin system, where the core application knows about specific plugin properties like `routes` or `adminUI`, to a generic one.
|
|
8
|
-
|
|
9
|
-
The core concept is a **"parts" system**. A "part" is a formally defined, named extension point in the application that a plugin can provide an implementation for. The core application doesn't know what a "GraphQL Plugin" is; it only knows how to find and render a "tool" in the admin bar, or how to register a "server route".
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## The Plugin Contract
|
|
14
|
-
|
|
15
|
-
We would redefine the `CMSPlugin` interface to be centered around this "parts" concept.
|
|
16
|
-
|
|
17
|
-
```typescript
|
|
18
|
-
/**
|
|
19
|
-
* A generic definition for any piece of functionality a plugin can provide.
|
|
20
|
-
*/
|
|
21
|
-
export interface PluginPart {
|
|
22
|
-
/**
|
|
23
|
-
* The name of the part this plugin implements.
|
|
24
|
-
* @example 'aphex/admin/tool', 'aphex/server/route'
|
|
25
|
-
*/
|
|
26
|
-
implements: string;
|
|
27
|
-
|
|
28
|
-
/** The actual implementation (can be a component, a function, etc.) */
|
|
29
|
-
component?: any; // In practice, a SvelteComponent constructor
|
|
30
|
-
|
|
31
|
-
/** The handler function for a route part */
|
|
32
|
-
handler?: (event: import('@sveltejs/kit').RequestEvent) => Response | Promise<Response>;
|
|
33
|
-
|
|
34
|
-
/** Other metadata the part might need for rendering or execution */
|
|
35
|
-
[key: string]: any;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* The new plugin contract. A plugin is a collection of parts.
|
|
40
|
-
*/
|
|
41
|
-
export interface CMSPlugin {
|
|
42
|
-
name: string;
|
|
43
|
-
version: string;
|
|
44
|
-
parts?: PluginPart[];
|
|
45
|
-
/** `install` can still be used for complex, one-time setup logic */
|
|
46
|
-
install?: (cms: any) => Promise<void>;
|
|
47
|
-
}
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
---
|
|
51
|
-
|
|
52
|
-
## Defining Core Parts
|
|
53
|
-
|
|
54
|
-
We would define a set of core "parts" for Aphex CMS. This list can grow over time as more extension points are needed.
|
|
55
|
-
|
|
56
|
-
- `aphex/server/route`: Implemented by plugins that need to add a server-side API endpoint. The part would include a `path` and a `handler`.
|
|
57
|
-
- `aphex/admin/tool`: For adding a top-level, navigable tool (like a tab) to the main admin UI. The part would include an `id`, `title`, and a `component` to render.
|
|
58
|
-
- `aphex/field/component`: For registering a custom Svelte component to use for a specific schema field type (e.g., a special string input, a map selector, etc.).
|
|
59
|
-
- `aphex/document/action`: For adding a custom action button to the document editor (e.g., "Duplicate", "Translate", "Preview").
|
|
60
|
-
|
|
61
|
-
---
|
|
62
|
-
|
|
63
|
-
## Implementation with SvelteKit and Svelte 5
|
|
64
|
-
|
|
65
|
-
This architecture fits beautifully with SvelteKit's and Svelte 5's features.
|
|
66
|
-
|
|
67
|
-
### The Part Resolver Service
|
|
68
|
-
|
|
69
|
-
A singleton service, the "Part Resolver," would be the heart of the system.
|
|
70
|
-
|
|
71
|
-
1. **Initialization:** At application startup, the main `hooks.server.ts` would process the `cmsConfig.plugins` array once. It would loop through every plugin and every part, organizing them into a `Map<string, PluginPart[]>`. For example, the key `'aphex/admin/tool'` would hold an array of all tool parts from all installed plugins.
|
|
72
|
-
2. **Injection:** This resolver instance would be attached to SvelteKit's `event.locals`, making it universally available in `load` functions, server-side hooks, and API routes without needing global variables.
|
|
73
|
-
|
|
74
|
-
### Rendering UI Parts with Svelte 5
|
|
75
|
-
|
|
76
|
-
Dynamically rendering plugin components becomes trivial and efficient with Svelte 5.
|
|
77
|
-
|
|
78
|
-
1. **Data Loading:** In the `+page.server.ts` for the admin UI, the `load` function would use the resolver from `locals` to fetch the necessary parts.
|
|
79
|
-
```typescript
|
|
80
|
-
// in /routes/admin/+page.server.ts
|
|
81
|
-
export async function load({ locals }) {
|
|
82
|
-
const { partResolver } = locals.aphexCMS;
|
|
83
|
-
const adminTools = partResolver.getParts('aphex/admin/tool');
|
|
84
|
-
return { adminTools };
|
|
85
|
-
}
|
|
86
|
-
```
|
|
87
|
-
2. **Dynamic Rendering:** The `AdminApp.svelte` component would receive `adminTools` as a prop. It can then dynamically render the tabs and their content using `<svelte:component>`.
|
|
88
|
-
|
|
89
|
-
```svelte
|
|
90
|
-
<!-- AdminApp.svelte -->
|
|
91
|
-
<script lang="ts">
|
|
92
|
-
let { adminTools = [] } = $props();
|
|
93
|
-
</script>
|
|
94
|
-
|
|
95
|
-
<Tabs.Root>
|
|
96
|
-
<Tabs.List>
|
|
97
|
-
<!-- Render a trigger for each tool -->
|
|
98
|
-
{#each adminTools as tool}
|
|
99
|
-
<Tabs.Trigger value={tool.id}>{tool.title}</Tabs.Trigger>
|
|
100
|
-
{/each}
|
|
101
|
-
</Tabs.List>
|
|
102
|
-
|
|
103
|
-
<!-- Render the content for each tool -->
|
|
104
|
-
{#each adminTools as tool}
|
|
105
|
-
<Tabs.Content value={tool.id}>
|
|
106
|
-
<svelte:component this={tool.component} />
|
|
107
|
-
</Tabs.Content>
|
|
108
|
-
{/each}
|
|
109
|
-
</Tabs.Root>
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
### Handling Server Parts in SvelteKit
|
|
113
|
-
|
|
114
|
-
The main `hooks.server.ts` would use the resolver to handle non-UI parts. For example, it would get all `aphex/server/route` parts and register their handlers in a dynamic routing map, similar to how `pluginRoutes` works now but in a fully generic way.
|
|
115
|
-
|
|
116
|
-
---
|
|
117
|
-
|
|
118
|
-
## Example: The GraphQL Plugin Revisited
|
|
119
|
-
|
|
120
|
-
Under this system, the GraphQL plugin becomes a clean, declarative manifest of its parts.
|
|
121
|
-
|
|
122
|
-
```typescript
|
|
123
|
-
// In packages/graphql-plugin/src/index.ts
|
|
124
|
-
import GraphQLTabComponent from './GraphQLTab.svelte';
|
|
125
|
-
import { createYoga } from 'graphql-yoga';
|
|
126
|
-
|
|
127
|
-
// ... (yoga setup)
|
|
128
|
-
|
|
129
|
-
export function createGraphQLPlugin(config: GraphQLPluginConfig = {}): CMSPlugin {
|
|
130
|
-
const endpoint = config.endpoint ?? '/api/graphql';
|
|
131
|
-
const yogaApp = /* ... create yoga instance ... */;
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
name: '@aphexcms/graphql-plugin',
|
|
135
|
-
parts: [
|
|
136
|
-
// Part 1: The API endpoint
|
|
137
|
-
{
|
|
138
|
-
implements: 'aphex/server/route',
|
|
139
|
-
path: endpoint,
|
|
140
|
-
handler: (event) => yogaApp.fetch(event.request, event)
|
|
141
|
-
},
|
|
142
|
-
// Part 2: The UI tab in the admin interface
|
|
143
|
-
{
|
|
144
|
-
implements: 'aphex/admin/tool',
|
|
145
|
-
id: 'graphql',
|
|
146
|
-
title: 'GraphQL',
|
|
147
|
-
component: GraphQLTabComponent
|
|
148
|
-
}
|
|
149
|
-
]
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
With this, the core system remains completely agnostic. It doesn't know what "GraphQL" is, it simply knows how to render a "tool" and route a "server route". This is the foundation of a truly configurable and powerful plugin architecture.
|