@momentumcms/server-core 0.2.0 → 0.4.0
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/CHANGELOG.md +33 -0
- package/index.cjs +71 -4
- package/index.js +68 -3
- package/package.json +1 -1
- package/src/index.d.ts +1 -1
- package/src/lib/upload-handler.d.ts +45 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
## 0.4.0 (2026-02-22)
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- SEO plugin recovery, E2E fixes, and CLI templates ([#37](https://github.com/DonaldMurillo/momentum-cms/pull/37), [#33](https://github.com/DonaldMurillo/momentum-cms/issues/33))
|
|
6
|
+
- blocks showcase with articles, pages, and UI fixes ([#36](https://github.com/DonaldMurillo/momentum-cms/pull/36))
|
|
7
|
+
|
|
8
|
+
### 🩹 Fixes
|
|
9
|
+
|
|
10
|
+
- resolve lint errors, fix vitest config excludes, and fix CLI template test assertion ([5124f72](https://github.com/DonaldMurillo/momentum-cms/commit/5124f72))
|
|
11
|
+
|
|
12
|
+
### ❤️ Thank You
|
|
13
|
+
|
|
14
|
+
- Claude Opus 4.6
|
|
15
|
+
- Donald Murillo @DonaldMurillo
|
|
16
|
+
|
|
17
|
+
## 0.3.0 (2026-02-20)
|
|
18
|
+
|
|
19
|
+
### 🚀 Features
|
|
20
|
+
|
|
21
|
+
- add article slugs, detail pages, live preview, and fix PATCH field hooks ([454b61c](https://github.com/DonaldMurillo/momentum-cms/commit/454b61c))
|
|
22
|
+
- add named tabs support with nested data grouping and UI improvements ([#30](https://github.com/DonaldMurillo/momentum-cms/pull/30))
|
|
23
|
+
|
|
24
|
+
### 🩹 Fixes
|
|
25
|
+
|
|
26
|
+
- address code review issues across admin, server-core, and e2e ([4664463](https://github.com/DonaldMurillo/momentum-cms/commit/4664463))
|
|
27
|
+
- add auth guard and MIME validation to PATCH upload route; fix pagination with client-side filtering ([#32](https://github.com/DonaldMurillo/momentum-cms/pull/32))
|
|
28
|
+
|
|
29
|
+
### ❤️ Thank You
|
|
30
|
+
|
|
31
|
+
- Claude Opus 4.6
|
|
32
|
+
- Donald Murillo @DonaldMurillo
|
|
33
|
+
|
|
1
34
|
## 0.2.0 (2026-02-17)
|
|
2
35
|
|
|
3
36
|
### 🚀 Features
|
package/index.cjs
CHANGED
|
@@ -54,6 +54,7 @@ __export(src_exports, {
|
|
|
54
54
|
getMomentumAPI: () => getMomentumAPI,
|
|
55
55
|
getSwaggerUIHTML: () => getSwaggerUIHTML,
|
|
56
56
|
getUploadConfig: () => getUploadConfig,
|
|
57
|
+
handleCollectionUpload: () => handleCollectionUpload,
|
|
57
58
|
handleFileDelete: () => handleFileDelete,
|
|
58
59
|
handleFileGet: () => handleFileGet,
|
|
59
60
|
handleUpload: () => handleUpload,
|
|
@@ -75,7 +76,8 @@ __export(src_exports, {
|
|
|
75
76
|
sanitizeErrorMessage: () => sanitizeErrorMessage,
|
|
76
77
|
sanitizeFilename: () => sanitizeFilename,
|
|
77
78
|
shouldRunSeeding: () => shouldRunSeeding,
|
|
78
|
-
startPublishScheduler: () => startPublishScheduler
|
|
79
|
+
startPublishScheduler: () => startPublishScheduler,
|
|
80
|
+
validateMimeType: () => validateMimeType2
|
|
79
81
|
});
|
|
80
82
|
module.exports = __toCommonJS(src_exports);
|
|
81
83
|
|
|
@@ -515,6 +517,9 @@ var MediaCollection = defineCollection({
|
|
|
515
517
|
singular: "Media",
|
|
516
518
|
plural: "Media"
|
|
517
519
|
},
|
|
520
|
+
upload: {
|
|
521
|
+
mimeTypes: ["image/*", "application/pdf", "video/*", "audio/*"]
|
|
522
|
+
},
|
|
518
523
|
admin: {
|
|
519
524
|
useAsTitle: "filename",
|
|
520
525
|
defaultColumns: ["filename", "mimeType", "filesize", "createdAt"]
|
|
@@ -535,7 +540,6 @@ var MediaCollection = defineCollection({
|
|
|
535
540
|
description: "File size in bytes"
|
|
536
541
|
}),
|
|
537
542
|
text("path", {
|
|
538
|
-
required: true,
|
|
539
543
|
label: "Storage Path",
|
|
540
544
|
description: "Path/key where the file is stored",
|
|
541
545
|
admin: {
|
|
@@ -839,6 +843,7 @@ async function runFieldHooks(hookType, fields, data, req, operation) {
|
|
|
839
843
|
}
|
|
840
844
|
const hooks = field.hooks?.[hookType];
|
|
841
845
|
if (hooks && hooks.length > 0) {
|
|
846
|
+
const fieldExistsInData = field.name in processedData;
|
|
842
847
|
let value = processedData[field.name];
|
|
843
848
|
for (const hook of hooks) {
|
|
844
849
|
const result = await Promise.resolve(
|
|
@@ -853,7 +858,9 @@ async function runFieldHooks(hookType, fields, data, req, operation) {
|
|
|
853
858
|
value = result;
|
|
854
859
|
}
|
|
855
860
|
}
|
|
856
|
-
|
|
861
|
+
if (fieldExistsInData || value !== void 0) {
|
|
862
|
+
processedData[field.name] = value;
|
|
863
|
+
}
|
|
857
864
|
}
|
|
858
865
|
if (field.type === "group" && processedData[field.name] && typeof processedData[field.name] === "object" && !Array.isArray(processedData[field.name])) {
|
|
859
866
|
processedData[field.name] = await runFieldHooks(
|
|
@@ -4137,6 +4144,64 @@ async function handleUpload(config, request) {
|
|
|
4137
4144
|
};
|
|
4138
4145
|
}
|
|
4139
4146
|
}
|
|
4147
|
+
async function handleCollectionUpload(globalConfig, request) {
|
|
4148
|
+
const { adapter } = globalConfig;
|
|
4149
|
+
const { file, user, fields, collectionSlug, collectionUpload } = request;
|
|
4150
|
+
const maxFileSize = collectionUpload.maxFileSize ?? globalConfig.maxFileSize ?? 10 * 1024 * 1024;
|
|
4151
|
+
const allowedMimeTypes = collectionUpload.mimeTypes ?? globalConfig.allowedMimeTypes ?? [];
|
|
4152
|
+
try {
|
|
4153
|
+
if (!user) {
|
|
4154
|
+
return {
|
|
4155
|
+
status: 401,
|
|
4156
|
+
error: "Authentication required to upload files"
|
|
4157
|
+
};
|
|
4158
|
+
}
|
|
4159
|
+
const sizeError = validateFileSize(file, maxFileSize);
|
|
4160
|
+
if (sizeError) {
|
|
4161
|
+
return { status: 400, error: sizeError };
|
|
4162
|
+
}
|
|
4163
|
+
const mimeError = validateMimeType2(file.mimeType, allowedMimeTypes);
|
|
4164
|
+
if (mimeError) {
|
|
4165
|
+
return { status: 400, error: mimeError };
|
|
4166
|
+
}
|
|
4167
|
+
if (file.buffer && file.buffer.length > 0) {
|
|
4168
|
+
const magicByteResult = validateMimeType(
|
|
4169
|
+
file.buffer,
|
|
4170
|
+
file.mimeType,
|
|
4171
|
+
allowedMimeTypes
|
|
4172
|
+
);
|
|
4173
|
+
if (!magicByteResult.valid) {
|
|
4174
|
+
return {
|
|
4175
|
+
status: 400,
|
|
4176
|
+
error: magicByteResult.error ?? "File content does not match claimed type"
|
|
4177
|
+
};
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
const storedFile = await adapter.upload(file);
|
|
4181
|
+
const docData = {
|
|
4182
|
+
...fields,
|
|
4183
|
+
filename: file.originalName,
|
|
4184
|
+
mimeType: file.mimeType,
|
|
4185
|
+
filesize: file.size,
|
|
4186
|
+
path: storedFile.path,
|
|
4187
|
+
url: storedFile.url
|
|
4188
|
+
};
|
|
4189
|
+
const api = getMomentumAPI().setContext({ user });
|
|
4190
|
+
const doc = await api.collection(collectionSlug).create(docData);
|
|
4191
|
+
return {
|
|
4192
|
+
status: 201,
|
|
4193
|
+
doc
|
|
4194
|
+
};
|
|
4195
|
+
} catch (error) {
|
|
4196
|
+
if (error instanceof Error) {
|
|
4197
|
+
if (error.message.includes("Access denied")) {
|
|
4198
|
+
return { status: 403, error: error.message };
|
|
4199
|
+
}
|
|
4200
|
+
return { status: 500, error: `Upload failed: ${error.message}` };
|
|
4201
|
+
}
|
|
4202
|
+
return { status: 500, error: "Upload failed: Unknown error" };
|
|
4203
|
+
}
|
|
4204
|
+
}
|
|
4140
4205
|
async function handleFileDelete(adapter, path) {
|
|
4141
4206
|
return adapter.delete(path);
|
|
4142
4207
|
}
|
|
@@ -5256,6 +5321,7 @@ function coerceCsvValue(value, fieldType) {
|
|
|
5256
5321
|
getMomentumAPI,
|
|
5257
5322
|
getSwaggerUIHTML,
|
|
5258
5323
|
getUploadConfig,
|
|
5324
|
+
handleCollectionUpload,
|
|
5259
5325
|
handleFileDelete,
|
|
5260
5326
|
handleFileGet,
|
|
5261
5327
|
handleUpload,
|
|
@@ -5277,5 +5343,6 @@ function coerceCsvValue(value, fieldType) {
|
|
|
5277
5343
|
sanitizeErrorMessage,
|
|
5278
5344
|
sanitizeFilename,
|
|
5279
5345
|
shouldRunSeeding,
|
|
5280
|
-
startPublishScheduler
|
|
5346
|
+
startPublishScheduler,
|
|
5347
|
+
validateMimeType
|
|
5281
5348
|
});
|
package/index.js
CHANGED
|
@@ -434,6 +434,9 @@ var MediaCollection = defineCollection({
|
|
|
434
434
|
singular: "Media",
|
|
435
435
|
plural: "Media"
|
|
436
436
|
},
|
|
437
|
+
upload: {
|
|
438
|
+
mimeTypes: ["image/*", "application/pdf", "video/*", "audio/*"]
|
|
439
|
+
},
|
|
437
440
|
admin: {
|
|
438
441
|
useAsTitle: "filename",
|
|
439
442
|
defaultColumns: ["filename", "mimeType", "filesize", "createdAt"]
|
|
@@ -454,7 +457,6 @@ var MediaCollection = defineCollection({
|
|
|
454
457
|
description: "File size in bytes"
|
|
455
458
|
}),
|
|
456
459
|
text("path", {
|
|
457
|
-
required: true,
|
|
458
460
|
label: "Storage Path",
|
|
459
461
|
description: "Path/key where the file is stored",
|
|
460
462
|
admin: {
|
|
@@ -758,6 +760,7 @@ async function runFieldHooks(hookType, fields, data, req, operation) {
|
|
|
758
760
|
}
|
|
759
761
|
const hooks = field.hooks?.[hookType];
|
|
760
762
|
if (hooks && hooks.length > 0) {
|
|
763
|
+
const fieldExistsInData = field.name in processedData;
|
|
761
764
|
let value = processedData[field.name];
|
|
762
765
|
for (const hook of hooks) {
|
|
763
766
|
const result = await Promise.resolve(
|
|
@@ -772,7 +775,9 @@ async function runFieldHooks(hookType, fields, data, req, operation) {
|
|
|
772
775
|
value = result;
|
|
773
776
|
}
|
|
774
777
|
}
|
|
775
|
-
|
|
778
|
+
if (fieldExistsInData || value !== void 0) {
|
|
779
|
+
processedData[field.name] = value;
|
|
780
|
+
}
|
|
776
781
|
}
|
|
777
782
|
if (field.type === "group" && processedData[field.name] && typeof processedData[field.name] === "object" && !Array.isArray(processedData[field.name])) {
|
|
778
783
|
processedData[field.name] = await runFieldHooks(
|
|
@@ -4069,6 +4074,64 @@ async function handleUpload(config, request) {
|
|
|
4069
4074
|
};
|
|
4070
4075
|
}
|
|
4071
4076
|
}
|
|
4077
|
+
async function handleCollectionUpload(globalConfig, request) {
|
|
4078
|
+
const { adapter } = globalConfig;
|
|
4079
|
+
const { file, user, fields, collectionSlug, collectionUpload } = request;
|
|
4080
|
+
const maxFileSize = collectionUpload.maxFileSize ?? globalConfig.maxFileSize ?? 10 * 1024 * 1024;
|
|
4081
|
+
const allowedMimeTypes = collectionUpload.mimeTypes ?? globalConfig.allowedMimeTypes ?? [];
|
|
4082
|
+
try {
|
|
4083
|
+
if (!user) {
|
|
4084
|
+
return {
|
|
4085
|
+
status: 401,
|
|
4086
|
+
error: "Authentication required to upload files"
|
|
4087
|
+
};
|
|
4088
|
+
}
|
|
4089
|
+
const sizeError = validateFileSize(file, maxFileSize);
|
|
4090
|
+
if (sizeError) {
|
|
4091
|
+
return { status: 400, error: sizeError };
|
|
4092
|
+
}
|
|
4093
|
+
const mimeError = validateMimeType2(file.mimeType, allowedMimeTypes);
|
|
4094
|
+
if (mimeError) {
|
|
4095
|
+
return { status: 400, error: mimeError };
|
|
4096
|
+
}
|
|
4097
|
+
if (file.buffer && file.buffer.length > 0) {
|
|
4098
|
+
const magicByteResult = validateMimeType(
|
|
4099
|
+
file.buffer,
|
|
4100
|
+
file.mimeType,
|
|
4101
|
+
allowedMimeTypes
|
|
4102
|
+
);
|
|
4103
|
+
if (!magicByteResult.valid) {
|
|
4104
|
+
return {
|
|
4105
|
+
status: 400,
|
|
4106
|
+
error: magicByteResult.error ?? "File content does not match claimed type"
|
|
4107
|
+
};
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
const storedFile = await adapter.upload(file);
|
|
4111
|
+
const docData = {
|
|
4112
|
+
...fields,
|
|
4113
|
+
filename: file.originalName,
|
|
4114
|
+
mimeType: file.mimeType,
|
|
4115
|
+
filesize: file.size,
|
|
4116
|
+
path: storedFile.path,
|
|
4117
|
+
url: storedFile.url
|
|
4118
|
+
};
|
|
4119
|
+
const api = getMomentumAPI().setContext({ user });
|
|
4120
|
+
const doc = await api.collection(collectionSlug).create(docData);
|
|
4121
|
+
return {
|
|
4122
|
+
status: 201,
|
|
4123
|
+
doc
|
|
4124
|
+
};
|
|
4125
|
+
} catch (error) {
|
|
4126
|
+
if (error instanceof Error) {
|
|
4127
|
+
if (error.message.includes("Access denied")) {
|
|
4128
|
+
return { status: 403, error: error.message };
|
|
4129
|
+
}
|
|
4130
|
+
return { status: 500, error: `Upload failed: ${error.message}` };
|
|
4131
|
+
}
|
|
4132
|
+
return { status: 500, error: "Upload failed: Unknown error" };
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4072
4135
|
async function handleFileDelete(adapter, path) {
|
|
4073
4136
|
return adapter.delete(path);
|
|
4074
4137
|
}
|
|
@@ -5187,6 +5250,7 @@ export {
|
|
|
5187
5250
|
getMomentumAPI,
|
|
5188
5251
|
getSwaggerUIHTML,
|
|
5189
5252
|
getUploadConfig,
|
|
5253
|
+
handleCollectionUpload,
|
|
5190
5254
|
handleFileDelete,
|
|
5191
5255
|
handleFileGet,
|
|
5192
5256
|
handleUpload,
|
|
@@ -5208,5 +5272,6 @@ export {
|
|
|
5208
5272
|
sanitizeErrorMessage,
|
|
5209
5273
|
sanitizeFilename,
|
|
5210
5274
|
shouldRunSeeding,
|
|
5211
|
-
startPublishScheduler
|
|
5275
|
+
startPublishScheduler,
|
|
5276
|
+
validateMimeType2 as validateMimeType
|
|
5212
5277
|
};
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { buildGraphQLSchema, type GraphQLContext } from './lib/graphql-schema';
|
|
|
12
12
|
export { executeGraphQL, type GraphQLRequestBody, type GraphQLResult } from './lib/graphql-handler';
|
|
13
13
|
export { GraphQLJSON } from './lib/graphql-scalars';
|
|
14
14
|
export { generateApiKey, hashApiKey, getKeyPrefix, isValidApiKeyFormat, generateApiKeyId, createAdapterApiKeyStore, createPostgresApiKeyStore, API_KEYS_TABLE_SQL_POSTGRES, API_KEYS_TABLE_SQL_SQLITE, type ApiKeyRecord, type CreateApiKeyResult, type CreateApiKeyOptions, type ApiKeyStore, } from './lib/api-keys';
|
|
15
|
-
export { handleUpload, handleFileDelete, handleFileGet, getUploadConfig, type UploadRequest, type UploadResponse, type UploadConfig, } from './lib/upload-handler';
|
|
15
|
+
export { handleUpload, handleCollectionUpload, handleFileDelete, handleFileGet, getUploadConfig, validateMimeType, type UploadRequest, type UploadResponse, type UploadConfig, type CollectionUploadRequest, type CollectionUploadResponse, } from './lib/upload-handler';
|
|
16
16
|
export { generateOpenAPISpec, type OpenAPIDocument, type OpenAPIGeneratorOptions, } from './lib/openapi-generator';
|
|
17
17
|
export { getSwaggerUIHTML } from './lib/swagger-ui-html';
|
|
18
18
|
export { renderPreviewHTML, type PreviewRenderOptions } from './lib/preview-renderer';
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Upload Handler for Momentum CMS
|
|
3
3
|
* Framework-agnostic file upload handling
|
|
4
4
|
*/
|
|
5
|
-
import type { StorageAdapter, UploadedFile, MomentumConfig, MediaDocument } from '@momentumcms/core';
|
|
5
|
+
import type { StorageAdapter, UploadedFile, MomentumConfig, MediaDocument, UploadCollectionConfig } from '@momentumcms/core';
|
|
6
6
|
import { type MomentumAPIContext } from './momentum-api';
|
|
7
7
|
/**
|
|
8
8
|
* Upload request from the client.
|
|
@@ -43,6 +43,11 @@ export interface UploadConfig {
|
|
|
43
43
|
* Get upload configuration from MomentumConfig.
|
|
44
44
|
*/
|
|
45
45
|
export declare function getUploadConfig(config: MomentumConfig): UploadConfig | null;
|
|
46
|
+
/**
|
|
47
|
+
* Validate claimed MIME type against an allow-list.
|
|
48
|
+
* Returns an error message if the type is not allowed, or null if OK.
|
|
49
|
+
*/
|
|
50
|
+
export declare function validateMimeType(mimeType: string, allowedTypes: string[]): string | null;
|
|
46
51
|
/**
|
|
47
52
|
* Handle file upload.
|
|
48
53
|
*
|
|
@@ -51,6 +56,45 @@ export declare function getUploadConfig(config: MomentumConfig): UploadConfig |
|
|
|
51
56
|
* @returns Upload response with created media document or error
|
|
52
57
|
*/
|
|
53
58
|
export declare function handleUpload(config: UploadConfig, request: UploadRequest): Promise<UploadResponse>;
|
|
59
|
+
/**
|
|
60
|
+
* Upload request for a collection-level upload.
|
|
61
|
+
* Used when POST /api/{slug} with multipart/form-data hits an upload collection.
|
|
62
|
+
*/
|
|
63
|
+
export interface CollectionUploadRequest {
|
|
64
|
+
/** The uploaded file */
|
|
65
|
+
file: UploadedFile;
|
|
66
|
+
/** User context for access control */
|
|
67
|
+
user?: MomentumAPIContext['user'];
|
|
68
|
+
/** Non-file form fields from multipart body (e.g., alt, title) */
|
|
69
|
+
fields: Record<string, unknown>;
|
|
70
|
+
/** Target collection slug */
|
|
71
|
+
collectionSlug: string;
|
|
72
|
+
/** Collection-level upload config */
|
|
73
|
+
collectionUpload: UploadCollectionConfig;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Response from a collection-level upload.
|
|
77
|
+
*/
|
|
78
|
+
export interface CollectionUploadResponse {
|
|
79
|
+
/** Created document (with auto-populated file metadata) */
|
|
80
|
+
doc?: Record<string, unknown>;
|
|
81
|
+
/** Error message if upload failed */
|
|
82
|
+
error?: string;
|
|
83
|
+
/** HTTP status code */
|
|
84
|
+
status: number;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Handle file upload for an upload collection.
|
|
88
|
+
* Stores the file, auto-populates metadata fields, merges with user-provided fields,
|
|
89
|
+
* and creates the document in the target collection.
|
|
90
|
+
*
|
|
91
|
+
* Collection-level config overrides global config for mimeTypes and maxFileSize.
|
|
92
|
+
*
|
|
93
|
+
* @param globalConfig - Global upload configuration (storage adapter, defaults)
|
|
94
|
+
* @param request - Collection upload request with file, user fields, and collection config
|
|
95
|
+
* @returns Response with created document or error
|
|
96
|
+
*/
|
|
97
|
+
export declare function handleCollectionUpload(globalConfig: UploadConfig, request: CollectionUploadRequest): Promise<CollectionUploadResponse>;
|
|
54
98
|
/**
|
|
55
99
|
* Handle file deletion.
|
|
56
100
|
*
|