@michaelthielemann/kestrel-images-default 5.0.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/LICENSE +201 -0
- package/README.md +35 -0
- package/dist/generate.d.ts +16 -0
- package/dist/generate.js +22 -0
- package/dist/impl.d.ts +95 -0
- package/dist/impl.js +584 -0
- package/dist/module.d.ts +299 -0
- package/dist/module.js +208 -0
- package/dist/sizes.d.ts +49 -0
- package/dist/sizes.js +47 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +47 -0
package/dist/module.d.ts
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type Context } from "@michaelthielemann/kestrel/context";
|
|
3
|
+
import { type Variant } from "./impl.ts";
|
|
4
|
+
export declare const configSchema: z.ZodObject<{
|
|
5
|
+
sizes: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodObject<{
|
|
6
|
+
name: z.ZodString;
|
|
7
|
+
width: z.ZodNumber;
|
|
8
|
+
height: z.ZodOptional<z.ZodNumber>;
|
|
9
|
+
fit: z.ZodDefault<z.ZodEnum<["inside", "cover"]>>;
|
|
10
|
+
format: z.ZodDefault<z.ZodEnum<["webp", "original"]>>;
|
|
11
|
+
quality: z.ZodDefault<z.ZodNumber>;
|
|
12
|
+
}, "strip", z.ZodTypeAny, {
|
|
13
|
+
name: string;
|
|
14
|
+
width: number;
|
|
15
|
+
fit: "inside" | "cover";
|
|
16
|
+
format: "webp" | "original";
|
|
17
|
+
quality: number;
|
|
18
|
+
height?: number | undefined;
|
|
19
|
+
}, {
|
|
20
|
+
name: string;
|
|
21
|
+
width: number;
|
|
22
|
+
height?: number | undefined;
|
|
23
|
+
fit?: "inside" | "cover" | undefined;
|
|
24
|
+
format?: "webp" | "original" | undefined;
|
|
25
|
+
quality?: number | undefined;
|
|
26
|
+
}>, {
|
|
27
|
+
name: string;
|
|
28
|
+
width: number;
|
|
29
|
+
fit: "inside" | "cover";
|
|
30
|
+
format: "webp" | "original";
|
|
31
|
+
quality: number;
|
|
32
|
+
height?: number | undefined;
|
|
33
|
+
}, {
|
|
34
|
+
name: string;
|
|
35
|
+
width: number;
|
|
36
|
+
height?: number | undefined;
|
|
37
|
+
fit?: "inside" | "cover" | undefined;
|
|
38
|
+
format?: "webp" | "original" | undefined;
|
|
39
|
+
quality?: number | undefined;
|
|
40
|
+
}>, "many">>;
|
|
41
|
+
prefix: z.ZodDefault<z.ZodString>;
|
|
42
|
+
publicPath: z.ZodDefault<z.ZodString>;
|
|
43
|
+
media: z.ZodDefault<z.ZodObject<{
|
|
44
|
+
collection: z.ZodString;
|
|
45
|
+
}, "strip", z.ZodTypeAny, {
|
|
46
|
+
collection: string;
|
|
47
|
+
}, {
|
|
48
|
+
collection: string;
|
|
49
|
+
}>>;
|
|
50
|
+
chunk: z.ZodDefault<z.ZodNumber>;
|
|
51
|
+
staleAfterMs: z.ZodDefault<z.ZodNumber>;
|
|
52
|
+
maxAttempts: z.ZodDefault<z.ZodNumber>;
|
|
53
|
+
}, "strict", z.ZodTypeAny, {
|
|
54
|
+
prefix: string;
|
|
55
|
+
publicPath: string;
|
|
56
|
+
media: {
|
|
57
|
+
collection: string;
|
|
58
|
+
};
|
|
59
|
+
chunk: number;
|
|
60
|
+
staleAfterMs: number;
|
|
61
|
+
maxAttempts: number;
|
|
62
|
+
sizes?: {
|
|
63
|
+
name: string;
|
|
64
|
+
width: number;
|
|
65
|
+
fit: "inside" | "cover";
|
|
66
|
+
format: "webp" | "original";
|
|
67
|
+
quality: number;
|
|
68
|
+
height?: number | undefined;
|
|
69
|
+
}[] | undefined;
|
|
70
|
+
}, {
|
|
71
|
+
sizes?: {
|
|
72
|
+
name: string;
|
|
73
|
+
width: number;
|
|
74
|
+
height?: number | undefined;
|
|
75
|
+
fit?: "inside" | "cover" | undefined;
|
|
76
|
+
format?: "webp" | "original" | undefined;
|
|
77
|
+
quality?: number | undefined;
|
|
78
|
+
}[] | undefined;
|
|
79
|
+
prefix?: string | undefined;
|
|
80
|
+
publicPath?: string | undefined;
|
|
81
|
+
media?: {
|
|
82
|
+
collection: string;
|
|
83
|
+
} | undefined;
|
|
84
|
+
chunk?: number | undefined;
|
|
85
|
+
staleAfterMs?: number | undefined;
|
|
86
|
+
maxAttempts?: number | undefined;
|
|
87
|
+
}>;
|
|
88
|
+
declare const _default: import("@michaelthielemann/kestrel/defineModule").ModuleDefinition<"images/default", {
|
|
89
|
+
register: (ctx: Context) => Promise<import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError<string>> | import("@michaelthielemann/kestrel/result").Ok<{
|
|
90
|
+
result: import("./sizes.ts").SizeRow[];
|
|
91
|
+
runId: string;
|
|
92
|
+
requestId?: string;
|
|
93
|
+
trigger: import("@michaelthielemann/kestrel/context").Trigger;
|
|
94
|
+
payload: Record<string, unknown>;
|
|
95
|
+
params: Record<string, string>;
|
|
96
|
+
headers: Record<string, string>;
|
|
97
|
+
files: import("@michaelthielemann/kestrel/context").UploadedFile[];
|
|
98
|
+
ip?: string;
|
|
99
|
+
fail(code: import("@michaelthielemann/kestrel/errors").CoreCode, message: string, details?: Record<string, unknown>): import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError>;
|
|
100
|
+
fail(error: import("@michaelthielemann/kestrel/errors").KestrelError): import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError>;
|
|
101
|
+
done(result: unknown): import("@michaelthielemann/kestrel/result").Ok<Context>;
|
|
102
|
+
}>>;
|
|
103
|
+
listSizes: (ctx: Context) => Promise<import("@michaelthielemann/kestrel/result").Ok<{
|
|
104
|
+
result: import("./sizes.ts").SizeRow[];
|
|
105
|
+
runId: string;
|
|
106
|
+
requestId?: string;
|
|
107
|
+
trigger: import("@michaelthielemann/kestrel/context").Trigger;
|
|
108
|
+
payload: Record<string, unknown>;
|
|
109
|
+
params: Record<string, string>;
|
|
110
|
+
headers: Record<string, string>;
|
|
111
|
+
files: import("@michaelthielemann/kestrel/context").UploadedFile[];
|
|
112
|
+
ip?: string;
|
|
113
|
+
fail(code: import("@michaelthielemann/kestrel/errors").CoreCode, message: string, details?: Record<string, unknown>): import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError>;
|
|
114
|
+
fail(error: import("@michaelthielemann/kestrel/errors").KestrelError): import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError>;
|
|
115
|
+
done(result: unknown): import("@michaelthielemann/kestrel/result").Ok<Context>;
|
|
116
|
+
}>>;
|
|
117
|
+
generate: (ctx: Context) => Promise<import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError<string>> | import("@michaelthielemann/kestrel/result").Ok<{
|
|
118
|
+
result: {
|
|
119
|
+
items: {
|
|
120
|
+
id: string;
|
|
121
|
+
variants: Variant[];
|
|
122
|
+
}[];
|
|
123
|
+
};
|
|
124
|
+
runId: string;
|
|
125
|
+
requestId?: string;
|
|
126
|
+
trigger: import("@michaelthielemann/kestrel/context").Trigger;
|
|
127
|
+
payload: Record<string, unknown>;
|
|
128
|
+
params: Record<string, string>;
|
|
129
|
+
headers: Record<string, string>;
|
|
130
|
+
files: import("@michaelthielemann/kestrel/context").UploadedFile[];
|
|
131
|
+
ip?: string;
|
|
132
|
+
fail(code: import("@michaelthielemann/kestrel/errors").CoreCode, message: string, details?: Record<string, unknown>): import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError>;
|
|
133
|
+
fail(error: import("@michaelthielemann/kestrel/errors").KestrelError): import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError>;
|
|
134
|
+
done(result: unknown): import("@michaelthielemann/kestrel/result").Ok<Context>;
|
|
135
|
+
}> | import("@michaelthielemann/kestrel/result").Ok<{
|
|
136
|
+
result: {
|
|
137
|
+
id: string;
|
|
138
|
+
variants: Variant[];
|
|
139
|
+
};
|
|
140
|
+
runId: string;
|
|
141
|
+
requestId?: string;
|
|
142
|
+
trigger: import("@michaelthielemann/kestrel/context").Trigger;
|
|
143
|
+
payload: Record<string, unknown>;
|
|
144
|
+
params: Record<string, string>;
|
|
145
|
+
headers: Record<string, string>;
|
|
146
|
+
files: import("@michaelthielemann/kestrel/context").UploadedFile[];
|
|
147
|
+
ip?: string;
|
|
148
|
+
fail(code: import("@michaelthielemann/kestrel/errors").CoreCode, message: string, details?: Record<string, unknown>): import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError>;
|
|
149
|
+
fail(error: import("@michaelthielemann/kestrel/errors").KestrelError): import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError>;
|
|
150
|
+
done(result: unknown): import("@michaelthielemann/kestrel/result").Ok<Context>;
|
|
151
|
+
}>>;
|
|
152
|
+
sync: (ctx: Context) => Promise<import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError<string>> | import("@michaelthielemann/kestrel/result").Ok<{
|
|
153
|
+
result: import("./impl.ts").Job;
|
|
154
|
+
runId: string;
|
|
155
|
+
requestId?: string;
|
|
156
|
+
trigger: import("@michaelthielemann/kestrel/context").Trigger;
|
|
157
|
+
payload: Record<string, unknown>;
|
|
158
|
+
params: Record<string, string>;
|
|
159
|
+
headers: Record<string, string>;
|
|
160
|
+
files: import("@michaelthielemann/kestrel/context").UploadedFile[];
|
|
161
|
+
ip?: string;
|
|
162
|
+
fail(code: import("@michaelthielemann/kestrel/errors").CoreCode, message: string, details?: Record<string, unknown>): import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError>;
|
|
163
|
+
fail(error: import("@michaelthielemann/kestrel/errors").KestrelError): import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError>;
|
|
164
|
+
done(result: unknown): import("@michaelthielemann/kestrel/result").Ok<Context>;
|
|
165
|
+
}>>;
|
|
166
|
+
resume: (ctx: Context) => Promise<import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError<string>> | import("@michaelthielemann/kestrel/result").Ok<{
|
|
167
|
+
result: import("./impl.ts").Job | null;
|
|
168
|
+
runId: string;
|
|
169
|
+
requestId?: string;
|
|
170
|
+
trigger: import("@michaelthielemann/kestrel/context").Trigger;
|
|
171
|
+
payload: Record<string, unknown>;
|
|
172
|
+
params: Record<string, string>;
|
|
173
|
+
headers: Record<string, string>;
|
|
174
|
+
files: import("@michaelthielemann/kestrel/context").UploadedFile[];
|
|
175
|
+
ip?: string;
|
|
176
|
+
fail(code: import("@michaelthielemann/kestrel/errors").CoreCode, message: string, details?: Record<string, unknown>): import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError>;
|
|
177
|
+
fail(error: import("@michaelthielemann/kestrel/errors").KestrelError): import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError>;
|
|
178
|
+
done(result: unknown): import("@michaelthielemann/kestrel/result").Ok<Context>;
|
|
179
|
+
}>>;
|
|
180
|
+
prune: (ctx: Context) => Promise<import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError<string>> | import("@michaelthielemann/kestrel/result").Ok<{
|
|
181
|
+
result: {
|
|
182
|
+
sizes: number;
|
|
183
|
+
variants: number;
|
|
184
|
+
};
|
|
185
|
+
runId: string;
|
|
186
|
+
requestId?: string;
|
|
187
|
+
trigger: import("@michaelthielemann/kestrel/context").Trigger;
|
|
188
|
+
payload: Record<string, unknown>;
|
|
189
|
+
params: Record<string, string>;
|
|
190
|
+
headers: Record<string, string>;
|
|
191
|
+
files: import("@michaelthielemann/kestrel/context").UploadedFile[];
|
|
192
|
+
ip?: string;
|
|
193
|
+
fail(code: import("@michaelthielemann/kestrel/errors").CoreCode, message: string, details?: Record<string, unknown>): import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError>;
|
|
194
|
+
fail(error: import("@michaelthielemann/kestrel/errors").KestrelError): import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError>;
|
|
195
|
+
done(result: unknown): import("@michaelthielemann/kestrel/result").Ok<Context>;
|
|
196
|
+
}>>;
|
|
197
|
+
readStatus: (ctx: Context) => Promise<import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError<string>> | import("@michaelthielemann/kestrel/result").Ok<{
|
|
198
|
+
result: import("./impl.ts").ImagesStatus;
|
|
199
|
+
runId: string;
|
|
200
|
+
requestId?: string;
|
|
201
|
+
trigger: import("@michaelthielemann/kestrel/context").Trigger;
|
|
202
|
+
payload: Record<string, unknown>;
|
|
203
|
+
params: Record<string, string>;
|
|
204
|
+
headers: Record<string, string>;
|
|
205
|
+
files: import("@michaelthielemann/kestrel/context").UploadedFile[];
|
|
206
|
+
ip?: string;
|
|
207
|
+
fail(code: import("@michaelthielemann/kestrel/errors").CoreCode, message: string, details?: Record<string, unknown>): import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError>;
|
|
208
|
+
fail(error: import("@michaelthielemann/kestrel/errors").KestrelError): import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError>;
|
|
209
|
+
done(result: unknown): import("@michaelthielemann/kestrel/result").Ok<Context>;
|
|
210
|
+
}>>;
|
|
211
|
+
remove: (ctx: Context) => Promise<import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError<string>> | import("@michaelthielemann/kestrel/result").Ok<Context>>;
|
|
212
|
+
removeMany: (ctx: Context) => Promise<import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError<string>> | import("@michaelthielemann/kestrel/result").Ok<Context>>;
|
|
213
|
+
attach: (ctx: Context) => Promise<import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError<string>> | import("@michaelthielemann/kestrel/result").Ok<{
|
|
214
|
+
result: {
|
|
215
|
+
variants: {
|
|
216
|
+
size: string;
|
|
217
|
+
width: number;
|
|
218
|
+
height: number;
|
|
219
|
+
format: string;
|
|
220
|
+
bytes: number;
|
|
221
|
+
state: Variant["state"];
|
|
222
|
+
path: string;
|
|
223
|
+
}[];
|
|
224
|
+
id?: unknown;
|
|
225
|
+
items?: unknown;
|
|
226
|
+
};
|
|
227
|
+
runId: string;
|
|
228
|
+
requestId?: string;
|
|
229
|
+
trigger: import("@michaelthielemann/kestrel/context").Trigger;
|
|
230
|
+
payload: Record<string, unknown>;
|
|
231
|
+
params: Record<string, string>;
|
|
232
|
+
headers: Record<string, string>;
|
|
233
|
+
files: import("@michaelthielemann/kestrel/context").UploadedFile[];
|
|
234
|
+
ip?: string;
|
|
235
|
+
fail(code: import("@michaelthielemann/kestrel/errors").CoreCode, message: string, details?: Record<string, unknown>): import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError>;
|
|
236
|
+
fail(error: import("@michaelthielemann/kestrel/errors").KestrelError): import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError>;
|
|
237
|
+
done(result: unknown): import("@michaelthielemann/kestrel/result").Ok<Context>;
|
|
238
|
+
}> | import("@michaelthielemann/kestrel/result").Ok<{
|
|
239
|
+
result: {
|
|
240
|
+
items: {
|
|
241
|
+
variants: {
|
|
242
|
+
size: string;
|
|
243
|
+
width: number;
|
|
244
|
+
height: number;
|
|
245
|
+
format: string;
|
|
246
|
+
bytes: number;
|
|
247
|
+
state: Variant["state"];
|
|
248
|
+
path: string;
|
|
249
|
+
}[];
|
|
250
|
+
}[];
|
|
251
|
+
id?: unknown;
|
|
252
|
+
};
|
|
253
|
+
runId: string;
|
|
254
|
+
requestId?: string;
|
|
255
|
+
trigger: import("@michaelthielemann/kestrel/context").Trigger;
|
|
256
|
+
payload: Record<string, unknown>;
|
|
257
|
+
params: Record<string, string>;
|
|
258
|
+
headers: Record<string, string>;
|
|
259
|
+
files: import("@michaelthielemann/kestrel/context").UploadedFile[];
|
|
260
|
+
ip?: string;
|
|
261
|
+
fail(code: import("@michaelthielemann/kestrel/errors").CoreCode, message: string, details?: Record<string, unknown>): import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError>;
|
|
262
|
+
fail(error: import("@michaelthielemann/kestrel/errors").KestrelError): import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError>;
|
|
263
|
+
done(result: unknown): import("@michaelthielemann/kestrel/result").Ok<Context>;
|
|
264
|
+
}>>;
|
|
265
|
+
serve: (ctx: Context) => Promise<import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError<string>> | import("@michaelthielemann/kestrel/result").Ok<{
|
|
266
|
+
result: import("@michaelthielemann/kestrel/context").BinaryResult;
|
|
267
|
+
runId: string;
|
|
268
|
+
requestId?: string;
|
|
269
|
+
trigger: import("@michaelthielemann/kestrel/context").Trigger;
|
|
270
|
+
payload: Record<string, unknown>;
|
|
271
|
+
params: Record<string, string>;
|
|
272
|
+
headers: Record<string, string>;
|
|
273
|
+
files: import("@michaelthielemann/kestrel/context").UploadedFile[];
|
|
274
|
+
ip?: string;
|
|
275
|
+
fail(code: import("@michaelthielemann/kestrel/errors").CoreCode, message: string, details?: Record<string, unknown>): import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError>;
|
|
276
|
+
fail(error: import("@michaelthielemann/kestrel/errors").KestrelError): import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError>;
|
|
277
|
+
done(result: unknown): import("@michaelthielemann/kestrel/result").Ok<Context>;
|
|
278
|
+
}>>;
|
|
279
|
+
export: (dir: string) => (ctx: Context) => Promise<import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError<string>> | import("@michaelthielemann/kestrel/result").Ok<{
|
|
280
|
+
result: {
|
|
281
|
+
variants: {
|
|
282
|
+
written: number;
|
|
283
|
+
skipped: number;
|
|
284
|
+
};
|
|
285
|
+
};
|
|
286
|
+
runId: string;
|
|
287
|
+
requestId?: string;
|
|
288
|
+
trigger: import("@michaelthielemann/kestrel/context").Trigger;
|
|
289
|
+
payload: Record<string, unknown>;
|
|
290
|
+
params: Record<string, string>;
|
|
291
|
+
headers: Record<string, string>;
|
|
292
|
+
files: import("@michaelthielemann/kestrel/context").UploadedFile[];
|
|
293
|
+
ip?: string;
|
|
294
|
+
fail(code: import("@michaelthielemann/kestrel/errors").CoreCode, message: string, details?: Record<string, unknown>): import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError>;
|
|
295
|
+
fail(error: import("@michaelthielemann/kestrel/errors").KestrelError): import("@michaelthielemann/kestrel/result").Err<import("@michaelthielemann/kestrel/errors").KestrelError>;
|
|
296
|
+
done(result: unknown): import("@michaelthielemann/kestrel/result").Ok<Context>;
|
|
297
|
+
}>>;
|
|
298
|
+
}>;
|
|
299
|
+
export default _default;
|
package/dist/module.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { BLOBSTORE } from "@michaelthielemann/kestrel-contracts/blobstore";
|
|
3
|
+
import { PERSISTENCE } from "@michaelthielemann/kestrel-contracts/persistence";
|
|
4
|
+
import { binaryResult, first, stepFactory } from "@michaelthielemann/kestrel/context";
|
|
5
|
+
import { defineModule } from "@michaelthielemann/kestrel/defineModule";
|
|
6
|
+
import { isErr, ok } from "@michaelthielemann/kestrel/result";
|
|
7
|
+
import { DEFAULT_MAX_ATTEMPTS, createImages } from "./impl.js";
|
|
8
|
+
import { sizeSchema } from "./sizes.js";
|
|
9
|
+
export const configSchema = z
|
|
10
|
+
.object({
|
|
11
|
+
sizes: z.array(sizeSchema).optional(),
|
|
12
|
+
prefix: z.string().regex(/^([A-Za-z0-9._-]+\/)+$/, 'prefix must be one or more "<segment>/" parts, e.g. "media-variants/"').default("media-variants/"),
|
|
13
|
+
publicPath: z.string().regex(/^\/[A-Za-z0-9._/-]*$/).default("/media"),
|
|
14
|
+
media: z.object({ collection: z.string().min(1) }).default({ collection: "media_items" }),
|
|
15
|
+
chunk: z.number().int().min(1).max(500).default(20),
|
|
16
|
+
staleAfterMs: z.number().int().positive().default(60000),
|
|
17
|
+
maxAttempts: z.number().int().positive().default(DEFAULT_MAX_ATTEMPTS),
|
|
18
|
+
})
|
|
19
|
+
.strict();
|
|
20
|
+
function mediaId(ctx) {
|
|
21
|
+
if (ctx.params.id)
|
|
22
|
+
return ctx.params.id;
|
|
23
|
+
return first(ctx.payload.id);
|
|
24
|
+
}
|
|
25
|
+
function attachedVariant(images, variant) {
|
|
26
|
+
return { size: variant.size, width: variant.width, height: variant.height, format: variant.format, bytes: variant.bytes, state: variant.state, path: images.publicPath(variant.mediaId, variant) };
|
|
27
|
+
}
|
|
28
|
+
const SIZE_SCHEMA = { type: "object", properties: { name: { type: "string" }, width: { type: "number" }, height: { type: ["number", "null"] }, fit: { type: "string", enum: ["inside", "cover"] }, format: { type: "string", enum: ["webp", "original"] }, quality: { type: "number" }, source: { type: "string", enum: ["default", "config", "registered"] }, updatedAt: { type: "number" } }, required: ["name", "width", "fit", "format", "quality", "source", "updatedAt"] };
|
|
29
|
+
const SIZE_INPUT_SCHEMA = { type: "object", properties: { name: { type: "string" }, width: { type: "integer", minimum: 16, maximum: 8192 }, height: { type: "integer", minimum: 16, maximum: 8192 }, fit: { type: "string", enum: ["inside", "cover"] }, format: { type: "string", enum: ["webp", "original"] }, quality: { type: "integer", minimum: 1, maximum: 100 } }, required: ["name", "width"] };
|
|
30
|
+
const VARIANT_SCHEMA = { type: "object", properties: { id: { type: "string" }, mediaId: { type: "string" }, size: { type: "string" }, spec: { type: "string" }, width: { type: "number" }, height: { type: "number" }, format: { type: "string" }, key: { type: "string" }, bytes: { type: "number" }, state: { type: "string", enum: ["pending", "done", "error", "failed"] }, error: { type: ["string", "null"] }, attempts: { type: "number" }, updatedAt: { type: "number" } }, required: ["id", "mediaId", "size", "spec", "width", "height", "format", "key", "bytes", "state", "error", "attempts", "updatedAt"] };
|
|
31
|
+
const JOB_SCHEMA = { type: "object", properties: { id: { type: "string" }, state: { type: "string", enum: ["running", "paused", "done", "error"] }, total: { type: "number" }, done: { type: "number" }, failed: { type: "number" }, cursor: { type: "string" }, startedAt: { type: "number" }, updatedAt: { type: "number" }, finishedAt: { type: ["number", "null"] }, error: { type: ["string", "null"] } }, required: ["id", "state", "total", "done", "failed", "cursor", "startedAt", "updatedAt", "finishedAt", "error"] };
|
|
32
|
+
const ATTACHED_VARIANT_SCHEMA = { type: "object", properties: { size: { type: "string" }, width: { type: "number" }, height: { type: "number" }, format: { type: "string" }, bytes: { type: "number" }, state: { type: "string", enum: ["pending", "done", "error", "failed"] }, path: { type: "string" } }, required: ["size", "width", "height", "format", "bytes", "state", "path"] };
|
|
33
|
+
export default defineModule({
|
|
34
|
+
name: "images/default",
|
|
35
|
+
provides: [],
|
|
36
|
+
requires: [BLOBSTORE, PERSISTENCE],
|
|
37
|
+
configSchema,
|
|
38
|
+
async setup(config, deps) {
|
|
39
|
+
return createImages(config, { blobs: deps.get(BLOBSTORE), db: deps.get(PERSISTENCE), logger: deps.logger });
|
|
40
|
+
},
|
|
41
|
+
steps: (images) => ({
|
|
42
|
+
register: async (ctx) => {
|
|
43
|
+
const sizes = await images.register(ctx.payload.sizes);
|
|
44
|
+
if (isErr(sizes))
|
|
45
|
+
return ctx.fail(sizes.error);
|
|
46
|
+
return ok({ ...ctx, result: sizes.value });
|
|
47
|
+
},
|
|
48
|
+
listSizes: async (ctx) => ok({ ...ctx, result: await images.sizes() }),
|
|
49
|
+
generate: async (ctx) => {
|
|
50
|
+
const idsPayload = ctx.payload.ids;
|
|
51
|
+
if (Array.isArray(idsPayload)) {
|
|
52
|
+
const items = [];
|
|
53
|
+
for (const id of idsPayload.filter((value) => typeof value === "string")) {
|
|
54
|
+
const known = await images.exists(id);
|
|
55
|
+
if (isErr(known))
|
|
56
|
+
return ctx.fail(known.error);
|
|
57
|
+
if (!known.value)
|
|
58
|
+
continue;
|
|
59
|
+
const variants = await images.generate(id);
|
|
60
|
+
if (isErr(variants))
|
|
61
|
+
return ctx.fail(variants.error);
|
|
62
|
+
items.push({ id, variants: variants.value });
|
|
63
|
+
}
|
|
64
|
+
return ok({ ...ctx, result: { items } });
|
|
65
|
+
}
|
|
66
|
+
const id = mediaId(ctx);
|
|
67
|
+
if (!id)
|
|
68
|
+
return ctx.fail("VALIDATION", "images: missing media id");
|
|
69
|
+
const known = await images.exists(id);
|
|
70
|
+
if (isErr(known))
|
|
71
|
+
return ctx.fail(known.error);
|
|
72
|
+
if (!known.value)
|
|
73
|
+
return ctx.fail("NOT_FOUND", `images: media/${id} not found`);
|
|
74
|
+
const variants = await images.generate(id);
|
|
75
|
+
if (isErr(variants))
|
|
76
|
+
return ctx.fail(variants.error);
|
|
77
|
+
return ok({ ...ctx, result: { id, variants: variants.value } });
|
|
78
|
+
},
|
|
79
|
+
sync: async (ctx) => {
|
|
80
|
+
const job = await images.sync();
|
|
81
|
+
if (isErr(job))
|
|
82
|
+
return ctx.fail(job.error);
|
|
83
|
+
return ok({ ...ctx, result: job.value });
|
|
84
|
+
},
|
|
85
|
+
resume: async (ctx) => {
|
|
86
|
+
const job = await images.resume();
|
|
87
|
+
if (isErr(job))
|
|
88
|
+
return ctx.fail(job.error);
|
|
89
|
+
return ok({ ...ctx, result: job.value });
|
|
90
|
+
},
|
|
91
|
+
prune: async (ctx) => {
|
|
92
|
+
const names = ctx.payload.sizes;
|
|
93
|
+
if (!Array.isArray(names) || names.some((name) => typeof name !== "string"))
|
|
94
|
+
return ctx.fail("VALIDATION", "images: missing sizes");
|
|
95
|
+
const pruned = await images.prune(names);
|
|
96
|
+
if (isErr(pruned))
|
|
97
|
+
return ctx.fail(pruned.error);
|
|
98
|
+
return ok({ ...ctx, result: pruned.value });
|
|
99
|
+
},
|
|
100
|
+
readStatus: async (ctx) => {
|
|
101
|
+
const status = await images.status();
|
|
102
|
+
if (isErr(status))
|
|
103
|
+
return ctx.fail(status.error);
|
|
104
|
+
return ok({ ...ctx, result: status.value });
|
|
105
|
+
},
|
|
106
|
+
remove: async (ctx) => {
|
|
107
|
+
if (!ctx.params.id)
|
|
108
|
+
return ctx.fail("VALIDATION", "images: missing id");
|
|
109
|
+
const removed = await images.remove(ctx.params.id);
|
|
110
|
+
if (isErr(removed))
|
|
111
|
+
return ctx.fail(removed.error);
|
|
112
|
+
return ok(ctx);
|
|
113
|
+
},
|
|
114
|
+
removeMany: async (ctx) => {
|
|
115
|
+
const ids = ctx.result?.ids;
|
|
116
|
+
if (!Array.isArray(ids))
|
|
117
|
+
throw new Error("images: removeMany: no ids in result");
|
|
118
|
+
for (const id of ids) {
|
|
119
|
+
const removed = await images.remove(id);
|
|
120
|
+
if (isErr(removed))
|
|
121
|
+
return ctx.fail(removed.error);
|
|
122
|
+
}
|
|
123
|
+
return ok(ctx);
|
|
124
|
+
},
|
|
125
|
+
attach: async (ctx) => {
|
|
126
|
+
const result = ctx.result;
|
|
127
|
+
if (typeof result?.id === "string") {
|
|
128
|
+
const byMedia = await images.variantsOf([result.id]);
|
|
129
|
+
if (isErr(byMedia))
|
|
130
|
+
return ctx.fail(byMedia.error);
|
|
131
|
+
const variants = byMedia.value.get(result.id) ?? [];
|
|
132
|
+
return ok({ ...ctx, result: { ...result, variants: variants.map((v) => attachedVariant(images, v)) } });
|
|
133
|
+
}
|
|
134
|
+
if (Array.isArray(result?.items)) {
|
|
135
|
+
const items = result.items;
|
|
136
|
+
const ids = items.map((item) => item.id).filter((id) => typeof id === "string");
|
|
137
|
+
const byMedia = await images.variantsOf(ids);
|
|
138
|
+
if (isErr(byMedia))
|
|
139
|
+
return ctx.fail(byMedia.error);
|
|
140
|
+
const withVariants = items.map((item) => {
|
|
141
|
+
const variants = typeof item.id === "string" ? (byMedia.value.get(item.id) ?? []) : [];
|
|
142
|
+
return { ...item, variants: variants.map((v) => attachedVariant(images, v)) };
|
|
143
|
+
});
|
|
144
|
+
return ok({ ...ctx, result: { ...result, items: withVariants } });
|
|
145
|
+
}
|
|
146
|
+
throw new Error("images: attach: no media item(s) in result");
|
|
147
|
+
},
|
|
148
|
+
serve: async (ctx) => {
|
|
149
|
+
const id = ctx.params.id;
|
|
150
|
+
const file = ctx.params.file;
|
|
151
|
+
if (!id || !file)
|
|
152
|
+
return ctx.fail("VALIDATION", "images: missing id or file");
|
|
153
|
+
const found = await images.read(id, file);
|
|
154
|
+
if (isErr(found))
|
|
155
|
+
return ctx.fail(found.error);
|
|
156
|
+
if (found.value === null) {
|
|
157
|
+
const failed = await images.failure(id, file);
|
|
158
|
+
if (isErr(failed))
|
|
159
|
+
return ctx.fail(failed.error);
|
|
160
|
+
if (failed.value)
|
|
161
|
+
return ctx.fail("NOT_FOUND", `images: variant ${failed.value.size} for media/${id} failed after ${failed.value.attempts} attempts: ${failed.value.error ?? "unknown error"}`);
|
|
162
|
+
return ctx.fail("NOT_FOUND", `images: media/${id}/variants/${file} not found`);
|
|
163
|
+
}
|
|
164
|
+
const result = binaryResult(found.value.data, found.value.contentType, file);
|
|
165
|
+
if (found.value.fallback)
|
|
166
|
+
result.headers = { "x-kestrel-variant": found.value.variant };
|
|
167
|
+
return ok({ ...ctx, result });
|
|
168
|
+
},
|
|
169
|
+
export: stepFactory((dir) => async (ctx) => {
|
|
170
|
+
const variants = await images.exportTo(dir);
|
|
171
|
+
if (isErr(variants))
|
|
172
|
+
return ctx.fail(variants.error);
|
|
173
|
+
// the step runs after media.export in the same pipeline; replacing ctx.result would drop that
|
|
174
|
+
// step's counts, so the variant counts are nested under their own key instead.
|
|
175
|
+
const previous = typeof ctx.result === "object" && ctx.result !== null && !Array.isArray(ctx.result) ? ctx.result : {};
|
|
176
|
+
return ok({ ...ctx, result: { ...previous, variants: variants.value } });
|
|
177
|
+
}),
|
|
178
|
+
}),
|
|
179
|
+
teardown: (images) => images.close(),
|
|
180
|
+
describe: () => ({
|
|
181
|
+
register: { summary: "Replace the sizes registered by this instance – code-declared sizes are held in memory, never stored", reads: [], writes: ["result"], input: { type: "object", properties: { sizes: { type: "array", items: SIZE_INPUT_SCHEMA } }, required: ["sizes"] }, output: { type: "array", items: SIZE_SCHEMA }, errors: { 400: "empty list or invalid size definition", 409: "a size collides with a config size" } },
|
|
182
|
+
listSizes: { summary: "Effective sizes (defaults/config merged with the sizes registered in this process)", reads: [], writes: ["result"], output: { type: "array", items: SIZE_SCHEMA } },
|
|
183
|
+
generate: { summary: "Generate variants for one media item (id from params.id or payload.id), or for each id in payload.ids (unknown ids are skipped)", reads: [], writes: ["result"], output: { oneOf: [{ type: "object", properties: { id: { type: "string" }, variants: { type: "array", items: VARIANT_SCHEMA } }, required: ["id", "variants"] }, { type: "object", properties: { items: { type: "array", items: { type: "object", properties: { id: { type: "string" }, variants: { type: "array", items: VARIANT_SCHEMA } }, required: ["id", "variants"] } } }, required: ["items"] }] }, errors: { 400: "missing media id", 404: "media item not found" } },
|
|
184
|
+
sync: { summary: "Start a sync job, or resume a paused/error/stale one", reads: [], writes: ["result"], output: JOB_SCHEMA, errors: { 409: "a fresh job is already running" } },
|
|
185
|
+
resume: { summary: "Resume a paused/error/stale job, or no-op (for cron)", reads: [], writes: ["result"], output: { oneOf: [JOB_SCHEMA, { type: "null" }] } },
|
|
186
|
+
prune: { summary: "Delete the variants of sizes that are no longer declared", reads: [], writes: ["result"], input: { type: "object", properties: { sizes: { type: "array", items: { type: "string" } } }, required: ["sizes"] }, output: { type: "object", properties: { sizes: { type: "number" }, variants: { type: "number" } }, required: ["sizes", "variants"] }, errors: { 400: "missing sizes, or a name is still declared or has no variants" } },
|
|
187
|
+
readStatus: {
|
|
188
|
+
summary: "Sizes with usage/variant counts, current job, and sizes whose variants are left over from a size the code no longer declares",
|
|
189
|
+
reads: [],
|
|
190
|
+
writes: ["result"],
|
|
191
|
+
output: {
|
|
192
|
+
type: "object",
|
|
193
|
+
properties: {
|
|
194
|
+
sizes: { type: "array", items: { type: "object", properties: { ...SIZE_SCHEMA.properties, used: { type: "boolean" }, variants: { type: "object", properties: { done: { type: "number" }, pending: { type: "number" }, error: { type: "number" }, failed: { type: "number" } }, required: ["done", "pending", "error", "failed"] } }, required: [...SIZE_SCHEMA.required, "used", "variants"] } },
|
|
195
|
+
job: { oneOf: [JOB_SCHEMA, { type: "null" }] },
|
|
196
|
+
orphaned: { type: "object", properties: { sizes: { type: "array", items: { type: "string" } }, variants: { type: "number" } }, required: ["sizes", "variants"] },
|
|
197
|
+
registrySeen: { type: "boolean" },
|
|
198
|
+
},
|
|
199
|
+
required: ["sizes", "job", "orphaned", "registrySeen"],
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
remove: { summary: "Delete one media item's variants (blob + rows)", reads: ["params.id"], writes: [], errors: { 400: "missing id" } },
|
|
203
|
+
removeMany: { summary: "Delete variants for every id in result.ids (from media.folderItems)", reads: ["result.ids"], writes: [] },
|
|
204
|
+
attach: { summary: "Add variants[] to a media item or list (result.id or result.items)", reads: ["result"], writes: ["result.variants"], extendsItems: { type: "object", properties: { variants: { type: "array", items: ATTACHED_VARIANT_SCHEMA } }, required: ["variants"] } },
|
|
205
|
+
serve: { summary: "Binary variant, or the original with x-kestrel-variant: pending while it isn't ready yet", reads: ["params.id", "params.file"], writes: ["result"], binary: true, errors: { 400: "missing id or file", 404: "media item or size not found, or the variant gave up after maxAttempts" } },
|
|
206
|
+
export: (dir) => ({ summary: `Copy every done variant to ${dir}/<folder>/<filename>.<size>.<ext>`, reads: [], writes: ["result.variants"], extendsOutput: { type: "object", properties: { variants: { type: "object", properties: { written: { type: "number" }, skipped: { type: "number" } }, required: ["written", "skipped"] } }, required: ["variants"] } }),
|
|
207
|
+
}),
|
|
208
|
+
});
|
package/dist/sizes.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type KestrelError } from "@michaelthielemann/kestrel/errors";
|
|
2
|
+
import { type Result } from "@michaelthielemann/kestrel/result";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
export type SizeSource = "default" | "config" | "registered";
|
|
5
|
+
export interface SizeRow extends Size {
|
|
6
|
+
source: SizeSource;
|
|
7
|
+
updatedAt: number;
|
|
8
|
+
}
|
|
9
|
+
export declare const DEFAULT_SIZES: Size[];
|
|
10
|
+
export declare const sizeSchema: z.ZodEffects<z.ZodObject<{
|
|
11
|
+
name: z.ZodString;
|
|
12
|
+
width: z.ZodNumber;
|
|
13
|
+
height: z.ZodOptional<z.ZodNumber>;
|
|
14
|
+
fit: z.ZodDefault<z.ZodEnum<["inside", "cover"]>>;
|
|
15
|
+
format: z.ZodDefault<z.ZodEnum<["webp", "original"]>>;
|
|
16
|
+
quality: z.ZodDefault<z.ZodNumber>;
|
|
17
|
+
}, "strip", z.ZodTypeAny, {
|
|
18
|
+
name: string;
|
|
19
|
+
width: number;
|
|
20
|
+
fit: "inside" | "cover";
|
|
21
|
+
format: "webp" | "original";
|
|
22
|
+
quality: number;
|
|
23
|
+
height?: number | undefined;
|
|
24
|
+
}, {
|
|
25
|
+
name: string;
|
|
26
|
+
width: number;
|
|
27
|
+
height?: number | undefined;
|
|
28
|
+
fit?: "inside" | "cover" | undefined;
|
|
29
|
+
format?: "webp" | "original" | undefined;
|
|
30
|
+
quality?: number | undefined;
|
|
31
|
+
}>, {
|
|
32
|
+
name: string;
|
|
33
|
+
width: number;
|
|
34
|
+
fit: "inside" | "cover";
|
|
35
|
+
format: "webp" | "original";
|
|
36
|
+
quality: number;
|
|
37
|
+
height?: number | undefined;
|
|
38
|
+
}, {
|
|
39
|
+
name: string;
|
|
40
|
+
width: number;
|
|
41
|
+
height?: number | undefined;
|
|
42
|
+
fit?: "inside" | "cover" | undefined;
|
|
43
|
+
format?: "webp" | "original" | undefined;
|
|
44
|
+
quality?: number | undefined;
|
|
45
|
+
}>;
|
|
46
|
+
export type Size = z.output<typeof sizeSchema>;
|
|
47
|
+
export declare function spec(size: Size): string;
|
|
48
|
+
export declare function extOf(size: Size, originalFormat: string): string;
|
|
49
|
+
export declare function mergeSizes(base: Size[], baseSource: "default" | "config", registered: Size[]): Result<SizeRow[], KestrelError<"CONFLICT">>;
|
package/dist/sizes.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { failure } from "@michaelthielemann/kestrel/errors";
|
|
2
|
+
import { err, ok } from "@michaelthielemann/kestrel/result";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
export const DEFAULT_SIZES = [
|
|
5
|
+
{ name: "thumb", width: 320, fit: "inside", format: "webp", quality: 82 },
|
|
6
|
+
{ name: "small", width: 640, fit: "inside", format: "webp", quality: 82 },
|
|
7
|
+
{ name: "medium", width: 1024, fit: "inside", format: "webp", quality: 82 },
|
|
8
|
+
{ name: "large", width: 1600, fit: "inside", format: "webp", quality: 82 },
|
|
9
|
+
{ name: "xl", width: 2400, fit: "inside", format: "webp", quality: 82 },
|
|
10
|
+
];
|
|
11
|
+
export const sizeSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
name: z.string().regex(/^[a-z][a-z0-9-]*$/),
|
|
14
|
+
width: z.number().int().min(16).max(8192),
|
|
15
|
+
height: z.number().int().min(16).max(8192).optional(),
|
|
16
|
+
fit: z.enum(["inside", "cover"]).default("inside"),
|
|
17
|
+
format: z.enum(["webp", "original"]).default("webp"),
|
|
18
|
+
quality: z.number().int().min(1).max(100).default(82),
|
|
19
|
+
})
|
|
20
|
+
.superRefine((size, ctx) => {
|
|
21
|
+
if (size.fit === "cover" && size.height === undefined) {
|
|
22
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "height is required for fit \"cover\"", path: ["height"] });
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
export function spec(size) {
|
|
26
|
+
return `${size.width}x${size.height ?? ""}:${size.fit}:${size.format}:${size.quality}`;
|
|
27
|
+
}
|
|
28
|
+
const EXT_ALIASES = { jpeg: "jpg" };
|
|
29
|
+
export function extOf(size, originalFormat) {
|
|
30
|
+
if (size.format === "webp")
|
|
31
|
+
return "webp";
|
|
32
|
+
return EXT_ALIASES[originalFormat] ?? originalFormat;
|
|
33
|
+
}
|
|
34
|
+
export function mergeSizes(base, baseSource, registered) {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
const rows = new Map();
|
|
37
|
+
for (const size of base)
|
|
38
|
+
rows.set(size.name, { ...size, source: baseSource, updatedAt: now });
|
|
39
|
+
for (const size of registered) {
|
|
40
|
+
const existing = rows.get(size.name);
|
|
41
|
+
if (baseSource === "config" && existing?.source === "config" && spec(existing) !== spec(size)) {
|
|
42
|
+
return err(failure("CONFLICT", `images: size "${size.name}" is defined in config`));
|
|
43
|
+
}
|
|
44
|
+
rows.set(size.name, { ...size, source: "registered", updatedAt: now });
|
|
45
|
+
}
|
|
46
|
+
return ok([...rows.values()]);
|
|
47
|
+
}
|