-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathstorage.zod.ts
More file actions
241 lines (221 loc) · 9.73 KB
/
storage.zod.ts
File metadata and controls
241 lines (221 loc) · 9.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
import { z } from 'zod';
import { BaseResponseSchema } from './contract.zod';
import { FileMetadataSchema } from '../system/object-storage.zod';
/**
* Storage Service Protocol
*
* Defines the API contract for client-side file operations.
* Focuses on secure, direct-to-cloud uploads (Presigned URLs)
* rather than proxying bytes through the API server.
*/
// ==========================================
// Requests
// ==========================================
export const GetPresignedUrlRequestSchema = z.object({
filename: z.string().describe('Original filename'),
mimeType: z.string().describe('File MIME type'),
size: z.number().describe('File size in bytes'),
scope: z.string().default('user').describe('Target storage scope (e.g. user, private, public)'),
bucket: z.string().optional().describe('Specific bucket override (admin only)'),
});
export const CompleteUploadRequestSchema = z.object({
fileId: z.string().describe('File ID returned from presigned request'),
eTag: z.string().optional().describe('S3 ETag verification'),
});
// ==========================================
// Responses
// ==========================================
export const PresignedUrlResponseSchema = BaseResponseSchema.extend({
data: z.object({
uploadUrl: z.string().describe('PUT/POST URL for direct upload'),
downloadUrl: z.string().optional().describe('Public/Private preview URL'),
fileId: z.string().describe('Temporary File ID'),
method: z.enum(['PUT', 'POST']).describe('HTTP Method to use'),
headers: z.record(z.string(), z.string()).optional().describe('Required headers for upload'),
expiresIn: z.number().describe('URL expiry in seconds'),
}),
});
export const FileUploadResponseSchema = BaseResponseSchema.extend({
data: FileMetadataSchema.describe('Uploaded file metadata'),
});
export type GetPresignedUrlRequest = z.infer<typeof GetPresignedUrlRequestSchema>;
export type CompleteUploadRequest = z.infer<typeof CompleteUploadRequestSchema>;
export type PresignedUrlResponse = z.infer<typeof PresignedUrlResponseSchema>;
export type FileUploadResponse = z.infer<typeof FileUploadResponseSchema>;
// ==========================================
// Chunked / Resumable Upload Protocol
// ==========================================
/**
* File Type Validation Schema
* Configures allowed and blocked file types for upload endpoints.
*
* @example Allow images only
* { mode: 'whitelist', mimeTypes: ['image/jpeg', 'image/png', 'image/webp'], maxFileSize: 10485760 }
*/
export const FileTypeValidationSchema = z.object({
mode: z.enum(['whitelist', 'blacklist'])
.describe('whitelist = only allow listed types, blacklist = block listed types'),
mimeTypes: z.array(z.string()).min(1)
.describe('List of MIME types to allow or block (e.g., "image/jpeg", "application/pdf")'),
extensions: z.array(z.string()).optional()
.describe('List of file extensions to allow or block (e.g., ".jpg", ".pdf")'),
maxFileSize: z.number().int().min(1).optional()
.describe('Maximum file size in bytes'),
minFileSize: z.number().int().min(0).optional()
.describe('Minimum file size in bytes (e.g., reject empty files)'),
});
export type FileTypeValidation = z.infer<typeof FileTypeValidationSchema>;
/**
* Initiate Chunked Upload Request
* Starts a resumable multipart upload session.
*
* @example POST /api/v1/storage/upload/chunked
* { filename: 'large-video.mp4', mimeType: 'video/mp4', totalSize: 1073741824, chunkSize: 5242880 }
*/
export const InitiateChunkedUploadRequestSchema = z.object({
filename: z.string().describe('Original filename'),
mimeType: z.string().describe('File MIME type'),
totalSize: z.number().int().min(1).describe('Total file size in bytes'),
chunkSize: z.number().int().min(5242880).default(5242880)
.describe('Size of each chunk in bytes (minimum 5MB per S3 spec)'),
scope: z.string().default('user').describe('Target storage scope'),
bucket: z.string().optional().describe('Specific bucket override (admin only)'),
metadata: z.record(z.string(), z.string()).optional().describe('Custom metadata key-value pairs'),
});
export type InitiateChunkedUploadRequest = z.infer<typeof InitiateChunkedUploadRequestSchema>;
/**
* Initiate Chunked Upload Response
* Returns a resume token and upload session details.
*/
export const InitiateChunkedUploadResponseSchema = BaseResponseSchema.extend({
data: z.object({
uploadId: z.string().describe('Multipart upload session ID'),
resumeToken: z.string().describe('Opaque token for resuming interrupted uploads'),
fileId: z.string().describe('Assigned file ID'),
totalChunks: z.number().int().min(1).describe('Expected number of chunks'),
chunkSize: z.number().int().describe('Chunk size in bytes'),
expiresAt: z.string().datetime().describe('Upload session expiration timestamp'),
}),
});
export type InitiateChunkedUploadResponse = z.infer<typeof InitiateChunkedUploadResponseSchema>;
/**
* Upload Chunk Request
* Uploads a single chunk of a multipart upload.
*
* @example PUT /api/v1/storage/upload/chunked/:uploadId/chunk/:chunkIndex
*/
export const UploadChunkRequestSchema = z.object({
uploadId: z.string().describe('Multipart upload session ID'),
chunkIndex: z.number().int().min(0).describe('Zero-based chunk index'),
resumeToken: z.string().describe('Resume token from initiate response'),
});
export type UploadChunkRequest = z.infer<typeof UploadChunkRequestSchema>;
/**
* Upload Chunk Response
* Confirms a single chunk upload with ETag for assembly.
*/
export const UploadChunkResponseSchema = BaseResponseSchema.extend({
data: z.object({
chunkIndex: z.number().int().describe('Chunk index that was uploaded'),
eTag: z.string().describe('Chunk ETag for multipart completion'),
bytesReceived: z.number().int().describe('Bytes received for this chunk'),
}),
});
export type UploadChunkResponse = z.infer<typeof UploadChunkResponseSchema>;
/**
* Complete Chunked Upload Request
* Assembles all uploaded chunks into a final file.
*
* @example POST /api/v1/storage/upload/chunked/:uploadId/complete
*/
export const CompleteChunkedUploadRequestSchema = z.object({
uploadId: z.string().describe('Multipart upload session ID'),
parts: z.array(z.object({
chunkIndex: z.number().int().describe('Chunk index'),
eTag: z.string().describe('ETag returned from chunk upload'),
})).min(1).describe('Ordered list of uploaded parts for assembly'),
});
export type CompleteChunkedUploadRequest = z.infer<typeof CompleteChunkedUploadRequestSchema>;
/**
* Complete Chunked Upload Response
* Confirms that all chunks have been assembled into the final file.
*/
export const CompleteChunkedUploadResponseSchema = BaseResponseSchema.extend({
data: z.object({
fileId: z.string().describe('Final file ID'),
key: z.string().describe('Storage key/path of the assembled file'),
size: z.number().int().describe('Total file size in bytes'),
mimeType: z.string().describe('File MIME type'),
eTag: z.string().optional().describe('Final ETag of the assembled file'),
url: z.string().optional().describe('Download URL for the assembled file'),
}),
});
export type CompleteChunkedUploadResponse = z.infer<typeof CompleteChunkedUploadResponseSchema>;
/**
* Upload Progress Schema
* Represents the current progress of an active upload session.
*
* @example GET /api/v1/storage/upload/chunked/:uploadId/progress
*/
export const UploadProgressSchema = BaseResponseSchema.extend({
data: z.object({
uploadId: z.string().describe('Multipart upload session ID'),
fileId: z.string().describe('Assigned file ID'),
filename: z.string().describe('Original filename'),
totalSize: z.number().int().describe('Total file size in bytes'),
uploadedSize: z.number().int().describe('Bytes uploaded so far'),
totalChunks: z.number().int().describe('Total expected chunks'),
uploadedChunks: z.number().int().describe('Number of chunks uploaded'),
percentComplete: z.number().min(0).max(100).describe('Upload progress percentage'),
status: z.enum(['in_progress', 'completing', 'completed', 'failed', 'expired'])
.describe('Current upload session status'),
startedAt: z.string().datetime().describe('Upload session start timestamp'),
expiresAt: z.string().datetime().describe('Session expiration timestamp'),
}),
});
export type UploadProgress = z.infer<typeof UploadProgressSchema>;
// ==========================================
// Storage API Contract Registry
// ==========================================
/**
* Standard Storage API contracts map.
* Used for generating SDKs, documentation, and route registration.
*/
export const StorageApiContracts = {
getPresignedUrl: {
method: 'POST' as const,
path: '/api/v1/storage/upload/presigned',
input: GetPresignedUrlRequestSchema,
output: PresignedUrlResponseSchema,
},
completeUpload: {
method: 'POST' as const,
path: '/api/v1/storage/upload/complete',
input: CompleteUploadRequestSchema,
output: FileUploadResponseSchema,
},
initiateChunkedUpload: {
method: 'POST' as const,
path: '/api/v1/storage/upload/chunked',
input: InitiateChunkedUploadRequestSchema,
output: InitiateChunkedUploadResponseSchema,
},
uploadChunk: {
method: 'PUT' as const,
path: '/api/v1/storage/upload/chunked/:uploadId/chunk/:chunkIndex',
input: UploadChunkRequestSchema,
output: UploadChunkResponseSchema,
},
completeChunkedUpload: {
method: 'POST' as const,
path: '/api/v1/storage/upload/chunked/:uploadId/complete',
input: CompleteChunkedUploadRequestSchema,
output: CompleteChunkedUploadResponseSchema,
},
getUploadProgress: {
method: 'GET' as const,
path: '/api/v1/storage/upload/chunked/:uploadId/progress',
output: UploadProgressSchema,
},
};