@natesena/blog-lib 0.1.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.
@@ -0,0 +1,475 @@
1
+ import {
2
+ GCSStorage,
3
+ __objRest,
4
+ __spreadProps,
5
+ __spreadValues
6
+ } from "./chunk-OPJV2ECE.js";
7
+
8
+ // src/sdk/utils/slug-generator.ts
9
+ import slugify from "slugify";
10
+ function generateSlug(title) {
11
+ const generatedSlug = slugify(title, { lower: true, strict: true });
12
+ if (!generatedSlug) {
13
+ return Date.now().toString(36);
14
+ }
15
+ return generatedSlug;
16
+ }
17
+
18
+ // src/sdk/errors.ts
19
+ var BlogLibError = class extends Error {
20
+ constructor(code, message) {
21
+ super(message);
22
+ this.name = "BlogLibError";
23
+ this.code = code;
24
+ }
25
+ };
26
+
27
+ // src/sdk/utils/validation.ts
28
+ function validateNonEmptyString(value, fieldName) {
29
+ if (typeof value !== "string" || value.trim().length === 0) {
30
+ throw new BlogLibError("VALIDATION_ERROR", `${fieldName} must be a non-empty string`);
31
+ }
32
+ }
33
+ function validateEmail(value, fieldName) {
34
+ validateNonEmptyString(value, fieldName);
35
+ const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
36
+ if (!emailPattern.test(value)) {
37
+ throw new BlogLibError("VALIDATION_ERROR", `${fieldName} must be a valid email address`);
38
+ }
39
+ }
40
+
41
+ // src/sdk/posts.ts
42
+ var PostsSDK = class {
43
+ constructor(adapter) {
44
+ this.adapter = adapter;
45
+ }
46
+ /**
47
+ * Create a new blog post.
48
+ * Generates a slug from the title if none is provided.
49
+ * Auto-sets publishedAt when status is PUBLISHED.
50
+ */
51
+ async create(input) {
52
+ validateNonEmptyString(input.title, "title");
53
+ validateNonEmptyString(input.authorId, "authorId");
54
+ validateNonEmptyString(input.content, "content");
55
+ const postSlug = input.slug || generateSlug(input.title);
56
+ const existingPostWithSlug = await this.adapter.getPostBySlug(postSlug);
57
+ if (existingPostWithSlug) {
58
+ throw new BlogLibError("DUPLICATE", `Post with slug "${postSlug}" already exists`);
59
+ }
60
+ const postPublishedAt = input.status === "PUBLISHED" && !input.publishedAt ? /* @__PURE__ */ new Date() : input.publishedAt;
61
+ return this.adapter.createPost(__spreadProps(__spreadValues({}, input), {
62
+ slug: postSlug,
63
+ publishedAt: postPublishedAt
64
+ }));
65
+ }
66
+ /** Get a post by its ID. */
67
+ async get(id) {
68
+ return this.adapter.getPost(id);
69
+ }
70
+ /** Get a post by its URL slug. */
71
+ async getBySlug(slug) {
72
+ return this.adapter.getPostBySlug(slug);
73
+ }
74
+ /**
75
+ * List posts with optional filtering and pagination.
76
+ * Defaults to limit=10, offset=0.
77
+ */
78
+ async list(filters) {
79
+ return this.adapter.listPosts(filters);
80
+ }
81
+ /**
82
+ * Update an existing post by ID.
83
+ * Auto-sets publishedAt when transitioning from DRAFT to PUBLISHED.
84
+ */
85
+ async update(id, data) {
86
+ const existingPost = await this.adapter.getPost(id);
87
+ if (!existingPost) {
88
+ throw new BlogLibError("NOT_FOUND", `Post with id "${id}" not found`);
89
+ }
90
+ const postPublishedAt = data.status === "PUBLISHED" && existingPost.status === "DRAFT" && !data.publishedAt ? /* @__PURE__ */ new Date() : data.publishedAt;
91
+ return this.adapter.updatePost(id, __spreadProps(__spreadValues({}, data), {
92
+ publishedAt: postPublishedAt
93
+ }));
94
+ }
95
+ /** Delete a post by ID. */
96
+ async delete(id) {
97
+ return this.adapter.deletePost(id);
98
+ }
99
+ };
100
+
101
+ // src/sdk/authors.ts
102
+ var AuthorsSDK = class {
103
+ constructor(adapter) {
104
+ this.adapter = adapter;
105
+ }
106
+ /** Create a new author. Email must be unique. */
107
+ async create(input) {
108
+ validateNonEmptyString(input.name, "name");
109
+ validateEmail(input.email, "email");
110
+ return this.adapter.createAuthor(input);
111
+ }
112
+ /** Get an author by their ID. */
113
+ async get(id) {
114
+ return this.adapter.getAuthor(id);
115
+ }
116
+ /** Get an author by their email address. */
117
+ async getByEmail(email) {
118
+ return this.adapter.getAuthorByEmail(email);
119
+ }
120
+ /** List authors with optional pagination. */
121
+ async list(options) {
122
+ return this.adapter.listAuthors(options);
123
+ }
124
+ /** Update an author by ID. */
125
+ async update(id, data) {
126
+ return this.adapter.updateAuthor(id, data);
127
+ }
128
+ /** Delete an author by ID. */
129
+ async delete(id) {
130
+ return this.adapter.deleteAuthor(id);
131
+ }
132
+ };
133
+
134
+ // src/sdk/tags.ts
135
+ var TagsSDK = class {
136
+ constructor(adapter) {
137
+ this.adapter = adapter;
138
+ }
139
+ /** Create a new tag. Name and slug must be provided and unique. */
140
+ async create(input) {
141
+ validateNonEmptyString(input.name, "name");
142
+ validateNonEmptyString(input.slug, "slug");
143
+ const existingTagWithSlug = await this.adapter.getTagBySlug(input.slug);
144
+ if (existingTagWithSlug) {
145
+ throw new BlogLibError("DUPLICATE", `Tag with slug "${input.slug}" already exists`);
146
+ }
147
+ return this.adapter.createTag(input);
148
+ }
149
+ /** Get a tag by its ID. */
150
+ async get(id) {
151
+ return this.adapter.getTag(id);
152
+ }
153
+ /** Get a tag by its URL slug. */
154
+ async getBySlug(slug) {
155
+ return this.adapter.getTagBySlug(slug);
156
+ }
157
+ /** List tags with optional pagination. */
158
+ async list(options) {
159
+ return this.adapter.listTags(options);
160
+ }
161
+ /** Update a tag by ID. */
162
+ async update(id, data) {
163
+ return this.adapter.updateTag(id, data);
164
+ }
165
+ /** Delete a tag by ID. */
166
+ async delete(id) {
167
+ return this.adapter.deleteTag(id);
168
+ }
169
+ };
170
+
171
+ // src/sdk/adapters/postgres.ts
172
+ var PostgresAdapter = class {
173
+ constructor(prisma) {
174
+ this.prisma = prisma;
175
+ }
176
+ async createPost(post) {
177
+ var _a;
178
+ const slug = post.slug || post.title.toLowerCase().replace(/\s+/g, "-");
179
+ const publishedAt = post.status === "PUBLISHED" && !post.publishedAt ? /* @__PURE__ */ new Date() : post.publishedAt || null;
180
+ const data = await this.prisma.post.create({
181
+ data: {
182
+ slug,
183
+ title: post.title,
184
+ content: post.content,
185
+ excerpt: post.excerpt || null,
186
+ coverImage: post.coverImage || null,
187
+ status: post.status,
188
+ publishedAt,
189
+ authorId: post.authorId,
190
+ tags: {
191
+ create: ((_a = post.tagIds) == null ? void 0 : _a.map((tagId) => ({
192
+ tag: { connect: { id: tagId } }
193
+ }))) || []
194
+ }
195
+ }
196
+ });
197
+ return {
198
+ id: data.id,
199
+ slug: data.slug,
200
+ title: data.title,
201
+ content: data.content,
202
+ excerpt: data.excerpt,
203
+ coverImage: data.coverImage,
204
+ status: data.status,
205
+ publishedAt: data.publishedAt,
206
+ createdAt: data.createdAt,
207
+ updatedAt: data.updatedAt,
208
+ authorId: data.authorId,
209
+ tagIds: post.tagIds || []
210
+ };
211
+ }
212
+ async getPost(id) {
213
+ const data = await this.prisma.post.findUnique({
214
+ where: { id },
215
+ include: {
216
+ author: true,
217
+ tags: { include: { tag: true } }
218
+ }
219
+ });
220
+ if (!data) return null;
221
+ return {
222
+ id: data.id,
223
+ slug: data.slug,
224
+ title: data.title,
225
+ content: data.content,
226
+ excerpt: data.excerpt,
227
+ coverImage: data.coverImage,
228
+ status: data.status,
229
+ publishedAt: data.publishedAt,
230
+ createdAt: data.createdAt,
231
+ updatedAt: data.updatedAt,
232
+ authorId: data.authorId,
233
+ tagIds: data.tags.map((postTag) => postTag.tagId)
234
+ };
235
+ }
236
+ async getPostBySlug(slug) {
237
+ const data = await this.prisma.post.findUnique({
238
+ where: { slug },
239
+ include: {
240
+ author: true,
241
+ tags: { include: { tag: true } }
242
+ }
243
+ });
244
+ if (!data) return null;
245
+ return {
246
+ id: data.id,
247
+ slug: data.slug,
248
+ title: data.title,
249
+ content: data.content,
250
+ excerpt: data.excerpt,
251
+ coverImage: data.coverImage,
252
+ status: data.status,
253
+ publishedAt: data.publishedAt,
254
+ createdAt: data.createdAt,
255
+ updatedAt: data.updatedAt,
256
+ authorId: data.authorId,
257
+ tagIds: data.tags.map((postTag) => postTag.tagId)
258
+ };
259
+ }
260
+ async listPosts(filters) {
261
+ const where = {};
262
+ if (filters == null ? void 0 : filters.status) {
263
+ where.status = filters.status;
264
+ }
265
+ if (filters == null ? void 0 : filters.authorId) {
266
+ where.authorId = filters.authorId;
267
+ }
268
+ if (filters == null ? void 0 : filters.tag) {
269
+ where.tags = {
270
+ some: {
271
+ tag: {
272
+ slug: filters.tag
273
+ }
274
+ }
275
+ };
276
+ }
277
+ if (filters == null ? void 0 : filters.publishedBefore) {
278
+ where.publishedAt = __spreadProps(__spreadValues({}, where.publishedAt), {
279
+ lt: filters.publishedBefore
280
+ });
281
+ }
282
+ if (filters == null ? void 0 : filters.publishedAfter) {
283
+ where.publishedAt = __spreadProps(__spreadValues({}, where.publishedAt), {
284
+ gt: filters.publishedAfter
285
+ });
286
+ }
287
+ const [posts, total] = await Promise.all([
288
+ this.prisma.post.findMany({
289
+ where,
290
+ orderBy: { createdAt: "desc" },
291
+ include: {
292
+ author: true,
293
+ tags: { include: { tag: true } }
294
+ },
295
+ take: (filters == null ? void 0 : filters.limit) || 10,
296
+ skip: (filters == null ? void 0 : filters.offset) || 0
297
+ }),
298
+ this.prisma.post.count({ where })
299
+ ]);
300
+ const transformedPosts = posts.map((post) => {
301
+ var _a;
302
+ return {
303
+ id: post.id,
304
+ slug: post.slug,
305
+ title: post.title,
306
+ content: post.content,
307
+ excerpt: post.excerpt,
308
+ coverImage: post.coverImage,
309
+ status: post.status,
310
+ publishedAt: post.publishedAt,
311
+ createdAt: post.createdAt,
312
+ updatedAt: post.updatedAt,
313
+ authorId: post.authorId,
314
+ tagIds: ((_a = post.tags) == null ? void 0 : _a.map((postTag) => postTag.tagId)) || []
315
+ };
316
+ });
317
+ return { posts: transformedPosts, total };
318
+ }
319
+ async updatePost(id, data) {
320
+ var _b;
321
+ const publishedAt = data.status === "PUBLISHED" ? /* @__PURE__ */ new Date() : void 0;
322
+ const _a = data, { tagIds: updatedTagIds } = _a, postFieldUpdates = __objRest(_a, ["tagIds"]);
323
+ if (updatedTagIds) {
324
+ await this.prisma.postTag.deleteMany({ where: { postId: id } });
325
+ }
326
+ const updated = await this.prisma.post.update({
327
+ where: { id },
328
+ data: __spreadValues(__spreadProps(__spreadValues({}, postFieldUpdates), {
329
+ publishedAt,
330
+ updatedAt: /* @__PURE__ */ new Date()
331
+ }), updatedTagIds ? {
332
+ tags: {
333
+ create: updatedTagIds.map((tagId) => ({
334
+ tag: { connect: { id: tagId } }
335
+ }))
336
+ }
337
+ } : {}),
338
+ include: {
339
+ author: true,
340
+ tags: { include: { tag: true } }
341
+ }
342
+ });
343
+ return {
344
+ id: updated.id,
345
+ slug: updated.slug,
346
+ title: updated.title,
347
+ content: updated.content,
348
+ excerpt: updated.excerpt,
349
+ coverImage: updated.coverImage,
350
+ status: updated.status,
351
+ publishedAt: updated.publishedAt,
352
+ createdAt: updated.createdAt,
353
+ updatedAt: updated.updatedAt,
354
+ authorId: updated.authorId,
355
+ tagIds: ((_b = updated.tags) == null ? void 0 : _b.map((postTag) => postTag.tagId)) || []
356
+ };
357
+ }
358
+ async deletePost(id) {
359
+ await this.prisma.post.delete({
360
+ where: { id }
361
+ });
362
+ }
363
+ async createAuthor(author) {
364
+ return await this.prisma.author.create({
365
+ data: author
366
+ });
367
+ }
368
+ async getAuthor(id) {
369
+ return await this.prisma.author.findUnique({
370
+ where: { id }
371
+ });
372
+ }
373
+ async getAuthorByEmail(email) {
374
+ return await this.prisma.author.findUnique({
375
+ where: { email }
376
+ });
377
+ }
378
+ async listAuthors(options) {
379
+ return await this.prisma.author.findMany({
380
+ orderBy: { name: "asc" },
381
+ take: (options == null ? void 0 : options.limit) || 100,
382
+ skip: (options == null ? void 0 : options.offset) || 0
383
+ });
384
+ }
385
+ async updateAuthor(id, data) {
386
+ return await this.prisma.author.update({
387
+ where: { id },
388
+ data: __spreadProps(__spreadValues({}, data), {
389
+ updatedAt: /* @__PURE__ */ new Date()
390
+ })
391
+ });
392
+ }
393
+ async deleteAuthor(id) {
394
+ await this.prisma.author.delete({
395
+ where: { id }
396
+ });
397
+ }
398
+ async createTag(tag) {
399
+ return await this.prisma.tag.create({
400
+ data: tag
401
+ });
402
+ }
403
+ async getTag(id) {
404
+ return await this.prisma.tag.findUnique({
405
+ where: { id }
406
+ });
407
+ }
408
+ async getTagBySlug(slug) {
409
+ return await this.prisma.tag.findUnique({
410
+ where: { slug }
411
+ });
412
+ }
413
+ async listTags(options) {
414
+ return await this.prisma.tag.findMany({
415
+ orderBy: { name: "asc" },
416
+ take: (options == null ? void 0 : options.limit) || 100,
417
+ skip: (options == null ? void 0 : options.offset) || 0
418
+ });
419
+ }
420
+ async updateTag(id, data) {
421
+ return await this.prisma.tag.update({
422
+ where: { id },
423
+ data: __spreadProps(__spreadValues({}, data), {
424
+ updatedAt: /* @__PURE__ */ new Date()
425
+ })
426
+ });
427
+ }
428
+ async deleteTag(id) {
429
+ await this.prisma.tag.delete({
430
+ where: { id }
431
+ });
432
+ }
433
+ // Slug generation is handled by PostsSDK using src/sdk/utils/slug-generator.ts
434
+ };
435
+
436
+ // src/sdk/index.ts
437
+ var BlogSDK = class _BlogSDK {
438
+ constructor(config) {
439
+ this.adapter = config.adapter;
440
+ this.posts = new PostsSDK(this.adapter);
441
+ this.authors = new AuthorsSDK(this.adapter);
442
+ this.tags = new TagsSDK(this.adapter);
443
+ if (config.gcs) {
444
+ this.gcsStorage = new GCSStorage(config.gcs);
445
+ }
446
+ }
447
+ /**
448
+ * Static factory method for initialization.
449
+ * Validates config and returns a ready-to-use BlogSDK instance.
450
+ */
451
+ static async initialize(config) {
452
+ return new _BlogSDK(config);
453
+ }
454
+ /**
455
+ * Upload an image to GCS. Requires gcs config in BlogSDKConfig.
456
+ * Returns { uploadUrl, publicUrl } — client uploads directly to uploadUrl.
457
+ */
458
+ async getImageUploadUrl(filename, contentType, options) {
459
+ if (!this.gcsStorage) {
460
+ throw new Error("GCS storage not configured. Pass gcs config to BlogSDK constructor.");
461
+ }
462
+ const destinationPath = (options == null ? void 0 : options.folder) ? `${options.folder}/${filename}` : filename;
463
+ return this.gcsStorage.getPresignedUploadUrl(destinationPath, contentType);
464
+ }
465
+ };
466
+
467
+ export {
468
+ BlogLibError,
469
+ PostsSDK,
470
+ AuthorsSDK,
471
+ TagsSDK,
472
+ PostgresAdapter,
473
+ BlogSDK
474
+ };
475
+ //# sourceMappingURL=chunk-EG563RZL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sdk/utils/slug-generator.ts","../src/sdk/errors.ts","../src/sdk/utils/validation.ts","../src/sdk/posts.ts","../src/sdk/authors.ts","../src/sdk/tags.ts","../src/sdk/adapters/postgres.ts","../src/sdk/index.ts"],"sourcesContent":["/**\n * Slug Generation Utility\n *\n * Converts a title string into a URL-friendly slug using the `slugify` package.\n * Handles unicode, transliteration, and diacritics properly.\n *\n * Ref: Plan Phase 5, Step 5.1\n */\n\nimport slugify from 'slugify';\n\n/**\n * Generate a URL-friendly slug from a title.\n * Handles unicode and diacritics: \"Café\" → \"cafe\", \"München\" → \"munchen\".\n *\n * @example generateSlug(\"Hello World!\") => \"hello-world\"\n * @example generateSlug(\"Café au Lait\") => \"cafe-au-lait\"\n * @example generateSlug(\"日本語\") => timestamp-based fallback\n */\nexport function generateSlug(title: string): string {\n const generatedSlug = slugify(title, { lower: true, strict: true });\n\n // Fallback for titles that produce empty slugs (e.g., only special characters)\n if (!generatedSlug) {\n return Date.now().toString(36);\n }\n\n return generatedSlug;\n}\n","/**\n * BlogLibError — Custom Error Class\n *\n * Provides structured error codes for programmatic error handling.\n * Consumers can catch BlogLibError and switch on the `code` field.\n *\n * Ref: Plan Phase 5, Step 5.3\n */\n\nexport type BlogLibErrorCode =\n | 'VALIDATION_ERROR'\n | 'NOT_FOUND'\n | 'DUPLICATE'\n | 'RELATION_ERROR'\n | 'STORAGE_ERROR'\n | 'AUTH_ERROR';\n\nexport class BlogLibError extends Error {\n readonly code: BlogLibErrorCode;\n\n constructor(code: BlogLibErrorCode, message: string) {\n super(message);\n this.name = 'BlogLibError';\n this.code = code;\n }\n}\n","/**\n * Input Validation Utilities\n *\n * Lightweight validation functions for SDK method inputs.\n * Throws BlogLibError with VALIDATION_ERROR code on failure.\n *\n * Ref: Plan Phase 5, Step 5.3\n */\n\nimport { BlogLibError } from '../errors';\n\n/**\n * Validate that a value is a non-empty string.\n * @throws BlogLibError with code VALIDATION_ERROR\n */\nexport function validateNonEmptyString(value: unknown, fieldName: string): asserts value is string {\n if (typeof value !== 'string' || value.trim().length === 0) {\n throw new BlogLibError('VALIDATION_ERROR', `${fieldName} must be a non-empty string`);\n }\n}\n\n/**\n * Validate that a value looks like an email address.\n * Basic format check — not a full RFC 5322 parser.\n * @throws BlogLibError with code VALIDATION_ERROR\n */\nexport function validateEmail(value: unknown, fieldName: string): asserts value is string {\n validateNonEmptyString(value, fieldName);\n const emailPattern = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n if (!emailPattern.test(value)) {\n throw new BlogLibError('VALIDATION_ERROR', `${fieldName} must be a valid email address`);\n }\n}\n\n/**\n * Validate that a value is one of the allowed options.\n * @throws BlogLibError with code VALIDATION_ERROR\n */\nexport function validateOneOf<T>(value: T, allowedValues: readonly T[], fieldName: string): void {\n if (!allowedValues.includes(value)) {\n throw new BlogLibError(\n 'VALIDATION_ERROR',\n `${fieldName} must be one of: ${allowedValues.join(', ')}`,\n );\n }\n}\n","/**\n * Posts SDK\n *\n * CRUD operations for blog posts, including filtering, slug lookup,\n * and automatic slug generation.\n *\n * Ref: docs/epic-blog-lib/02-architecture.md (lines 169-228)\n * Ref: docs/epic-blog-lib/03-tasks.md (Task openclaw-n1y.4)\n */\n\nimport type { DatabaseAdapter, AdapterPost } from './adapters/base';\nimport type { CreatePostInput, UpdatePostInput, PostListFilters } from '../types';\nimport { generateSlug } from './utils/slug-generator';\nimport { validateNonEmptyString } from './utils/validation';\nimport { BlogLibError } from './errors';\n\nexport class PostsSDK {\n private adapter: DatabaseAdapter;\n\n constructor(adapter: DatabaseAdapter) {\n this.adapter = adapter;\n }\n\n /**\n * Create a new blog post.\n * Generates a slug from the title if none is provided.\n * Auto-sets publishedAt when status is PUBLISHED.\n */\n async create(input: CreatePostInput): Promise<AdapterPost> {\n validateNonEmptyString(input.title, 'title');\n validateNonEmptyString(input.authorId, 'authorId');\n validateNonEmptyString(input.content, 'content');\n\n const postSlug = input.slug || generateSlug(input.title);\n\n const existingPostWithSlug = await this.adapter.getPostBySlug(postSlug);\n if (existingPostWithSlug) {\n throw new BlogLibError('DUPLICATE', `Post with slug \"${postSlug}\" already exists`);\n }\n\n const postPublishedAt =\n input.status === 'PUBLISHED' && !input.publishedAt\n ? new Date()\n : input.publishedAt;\n\n return this.adapter.createPost({\n ...input,\n slug: postSlug,\n publishedAt: postPublishedAt,\n });\n }\n\n /** Get a post by its ID. */\n async get(id: string): Promise<AdapterPost | null> {\n return this.adapter.getPost(id);\n }\n\n /** Get a post by its URL slug. */\n async getBySlug(slug: string): Promise<AdapterPost | null> {\n return this.adapter.getPostBySlug(slug);\n }\n\n /**\n * List posts with optional filtering and pagination.\n * Defaults to limit=10, offset=0.\n */\n async list(filters?: PostListFilters): Promise<{ posts: AdapterPost[]; total: number }> {\n return this.adapter.listPosts(filters);\n }\n\n /**\n * Update an existing post by ID.\n * Auto-sets publishedAt when transitioning from DRAFT to PUBLISHED.\n */\n async update(id: string, data: UpdatePostInput): Promise<AdapterPost> {\n const existingPost = await this.adapter.getPost(id);\n if (!existingPost) {\n throw new BlogLibError('NOT_FOUND', `Post with id \"${id}\" not found`);\n }\n\n const postPublishedAt =\n data.status === 'PUBLISHED' &&\n existingPost.status === 'DRAFT' &&\n !data.publishedAt\n ? new Date()\n : data.publishedAt;\n\n return this.adapter.updatePost(id, {\n ...data,\n publishedAt: postPublishedAt,\n });\n }\n\n /** Delete a post by ID. */\n async delete(id: string): Promise<void> {\n return this.adapter.deletePost(id);\n }\n}\n","/**\n * Authors SDK\n *\n * CRUD operations for blog authors.\n *\n * Ref: docs/epic-blog-lib/02-architecture.md (lines 213-218)\n * Ref: Plan Phase 5, Steps 5.3 and 5.4\n */\n\nimport type { DatabaseAdapter } from './adapters/base';\nimport type { Author, CreateAuthorInput, UpdateAuthorInput } from '../types';\nimport { validateNonEmptyString, validateEmail } from './utils/validation';\n\nexport class AuthorsSDK {\n private adapter: DatabaseAdapter;\n\n constructor(adapter: DatabaseAdapter) {\n this.adapter = adapter;\n }\n\n /** Create a new author. Email must be unique. */\n async create(input: CreateAuthorInput): Promise<Author> {\n validateNonEmptyString(input.name, 'name');\n validateEmail(input.email, 'email');\n\n return this.adapter.createAuthor(input);\n }\n\n /** Get an author by their ID. */\n async get(id: string): Promise<Author | null> {\n return this.adapter.getAuthor(id);\n }\n\n /** Get an author by their email address. */\n async getByEmail(email: string): Promise<Author | null> {\n return this.adapter.getAuthorByEmail(email);\n }\n\n /** List authors with optional pagination. */\n async list(options?: { limit?: number; offset?: number }): Promise<Author[]> {\n return this.adapter.listAuthors(options);\n }\n\n /** Update an author by ID. */\n async update(id: string, data: UpdateAuthorInput): Promise<Author> {\n return this.adapter.updateAuthor(id, data);\n }\n\n /** Delete an author by ID. */\n async delete(id: string): Promise<void> {\n return this.adapter.deleteAuthor(id);\n }\n}\n","/**\n * Tags SDK\n *\n * CRUD operations for blog tags.\n *\n * Ref: docs/epic-blog-lib/02-architecture.md (lines 220-225)\n * Ref: docs/epic-blog-lib/03-tasks.md (Task openclaw-n1y.14)\n */\n\nimport type { DatabaseAdapter } from './adapters/base';\nimport type { Tag, CreateTagInput, UpdateTagInput } from '../types';\nimport { validateNonEmptyString } from './utils/validation';\nimport { BlogLibError } from './errors';\n\nexport class TagsSDK {\n private adapter: DatabaseAdapter;\n\n constructor(adapter: DatabaseAdapter) {\n this.adapter = adapter;\n }\n\n /** Create a new tag. Name and slug must be provided and unique. */\n async create(input: CreateTagInput): Promise<Tag> {\n validateNonEmptyString(input.name, 'name');\n validateNonEmptyString(input.slug, 'slug');\n\n const existingTagWithSlug = await this.adapter.getTagBySlug(input.slug);\n if (existingTagWithSlug) {\n throw new BlogLibError('DUPLICATE', `Tag with slug \"${input.slug}\" already exists`);\n }\n\n return this.adapter.createTag(input);\n }\n\n /** Get a tag by its ID. */\n async get(id: string): Promise<Tag | null> {\n return this.adapter.getTag(id);\n }\n\n /** Get a tag by its URL slug. */\n async getBySlug(slug: string): Promise<Tag | null> {\n return this.adapter.getTagBySlug(slug);\n }\n\n /** List tags with optional pagination. */\n async list(options?: { limit?: number; offset?: number }): Promise<Tag[]> {\n return this.adapter.listTags(options);\n }\n\n /** Update a tag by ID. */\n async update(id: string, data: UpdateTagInput): Promise<Tag> {\n return this.adapter.updateTag(id, data);\n }\n\n /** Delete a tag by ID. */\n async delete(id: string): Promise<void> {\n return this.adapter.deleteTag(id);\n }\n}\n","/**\n * Postgres Adapter (Prisma)\n * \n * Implementation of DatabaseAdapter using Prisma ORM for PostgreSQL.\n * Handles all CRUD operations with proper error handling and validation.\n */\n\nimport { PrismaClient, type Author, type Tag } from '@prisma/client';\nimport type {\n DatabaseAdapter,\n CreatePostInput,\n UpdatePostInput,\n PostListFilters,\n CreateAuthorInput,\n UpdateAuthorInput,\n CreateTagInput,\n UpdateTagInput,\n AdapterPost,\n} from './base';\n\nexport class PostgresAdapter implements DatabaseAdapter {\n private prisma: PrismaClient;\n\n constructor(prisma: PrismaClient) {\n this.prisma = prisma;\n }\n\n async createPost(post: CreatePostInput): Promise<AdapterPost> {\n // Slug is expected to be provided by PostsSDK.create() — use title fallback as safety net\n const slug = post.slug || post.title.toLowerCase().replace(/\\s+/g, '-');\n const publishedAt = post.status === 'PUBLISHED' && !post.publishedAt\n ? new Date()\n : post.publishedAt || null;\n\n const data = await this.prisma.post.create({\n data: {\n slug,\n title: post.title,\n content: post.content,\n excerpt: post.excerpt || null,\n coverImage: post.coverImage || null,\n status: post.status,\n publishedAt,\n authorId: post.authorId,\n tags: {\n create: post.tagIds?.map((tagId) => ({\n tag: { connect: { id: tagId } },\n })) || [],\n },\n },\n });\n\n return {\n id: data.id,\n slug: data.slug,\n title: data.title,\n content: data.content,\n excerpt: data.excerpt,\n coverImage: data.coverImage,\n status: data.status,\n publishedAt: data.publishedAt,\n createdAt: data.createdAt,\n updatedAt: data.updatedAt,\n authorId: data.authorId,\n tagIds: post.tagIds || [],\n };\n }\n\n async getPost(id: string): Promise<AdapterPost | null> {\n const data = await this.prisma.post.findUnique({\n where: { id },\n include: {\n author: true,\n tags: { include: { tag: true } },\n },\n });\n\n if (!data) return null;\n\n return {\n id: data.id,\n slug: data.slug,\n title: data.title,\n content: data.content,\n excerpt: data.excerpt,\n coverImage: data.coverImage,\n status: data.status,\n publishedAt: data.publishedAt,\n createdAt: data.createdAt,\n updatedAt: data.updatedAt,\n authorId: data.authorId,\n tagIds: data.tags.map((postTag: any) => postTag.tagId),\n };\n }\n\n async getPostBySlug(slug: string): Promise<AdapterPost | null> {\n const data = await this.prisma.post.findUnique({\n where: { slug },\n include: {\n author: true,\n tags: { include: { tag: true } },\n },\n });\n\n if (!data) return null;\n\n return {\n id: data.id,\n slug: data.slug,\n title: data.title,\n content: data.content,\n excerpt: data.excerpt,\n coverImage: data.coverImage,\n status: data.status,\n publishedAt: data.publishedAt,\n createdAt: data.createdAt,\n updatedAt: data.updatedAt,\n authorId: data.authorId,\n tagIds: data.tags.map((postTag: any) => postTag.tagId),\n };\n }\n\n async listPosts(filters?: PostListFilters): Promise<{ posts: AdapterPost[]; total: number }> {\n const where: any = {};\n\n if (filters?.status) {\n where.status = filters.status;\n }\n\n if (filters?.authorId) {\n where.authorId = filters.authorId;\n }\n\n if (filters?.tag) {\n where.tags = {\n some: {\n tag: {\n slug: filters.tag,\n },\n },\n };\n }\n\n if (filters?.publishedBefore) {\n where.publishedAt = {\n ...where.publishedAt,\n lt: filters.publishedBefore,\n };\n }\n\n if (filters?.publishedAfter) {\n where.publishedAt = {\n ...where.publishedAt,\n gt: filters.publishedAfter,\n };\n }\n\n const [posts, total] = await Promise.all([\n this.prisma.post.findMany({\n where,\n orderBy: { createdAt: 'desc' },\n include: {\n author: true,\n tags: { include: { tag: true } },\n },\n take: filters?.limit || 10,\n skip: filters?.offset || 0,\n }),\n\n this.prisma.post.count({ where }),\n ]);\n\n const transformedPosts = posts.map((post: any) => ({\n id: post.id,\n slug: post.slug,\n title: post.title,\n content: post.content,\n excerpt: post.excerpt,\n coverImage: post.coverImage,\n status: post.status,\n publishedAt: post.publishedAt,\n createdAt: post.createdAt,\n updatedAt: post.updatedAt,\n authorId: post.authorId,\n tagIds: post.tags?.map((postTag: any) => postTag.tagId) || [],\n }));\n\n return { posts: transformedPosts, total };\n }\n\n async updatePost(id: string, data: UpdatePostInput): Promise<AdapterPost> {\n const publishedAt = data.status === 'PUBLISHED' ? new Date() : undefined;\n\n // Separate tagIds from the rest of the data to avoid passing it to Prisma's update\n const { tagIds: updatedTagIds, ...postFieldUpdates } = data;\n\n // If tagIds are provided, replace all tags using delete-then-create pattern\n // Ref: Plan Phase 2, Step 2.4\n if (updatedTagIds) {\n await this.prisma.postTag.deleteMany({ where: { postId: id } });\n }\n\n const updated = await this.prisma.post.update({\n where: { id },\n data: {\n ...postFieldUpdates,\n publishedAt,\n updatedAt: new Date(),\n ...(updatedTagIds ? {\n tags: {\n create: updatedTagIds.map((tagId: string) => ({\n tag: { connect: { id: tagId } },\n })),\n },\n } : {}),\n },\n include: {\n author: true,\n tags: { include: { tag: true } },\n },\n });\n\n return {\n id: updated.id,\n slug: updated.slug,\n title: updated.title,\n content: updated.content,\n excerpt: updated.excerpt,\n coverImage: updated.coverImage,\n status: updated.status,\n publishedAt: updated.publishedAt,\n createdAt: updated.createdAt,\n updatedAt: updated.updatedAt,\n authorId: updated.authorId,\n tagIds: updated.tags?.map((postTag: any) => postTag.tagId) || [],\n };\n }\n\n async deletePost(id: string): Promise<void> {\n // PostTag rows are cascade-deleted via onDelete: Cascade in schema\n await this.prisma.post.delete({\n where: { id },\n });\n }\n\n async createAuthor(author: CreateAuthorInput): Promise<Author> {\n return await this.prisma.author.create({\n data: author,\n });\n }\n\n async getAuthor(id: string): Promise<Author | null> {\n return await this.prisma.author.findUnique({\n where: { id },\n });\n }\n\n async getAuthorByEmail(email: string): Promise<Author | null> {\n return await this.prisma.author.findUnique({\n where: { email },\n });\n }\n\n async listAuthors(options?: { limit?: number; offset?: number }): Promise<Author[]> {\n return await this.prisma.author.findMany({\n orderBy: { name: 'asc' },\n take: options?.limit || 100,\n skip: options?.offset || 0,\n });\n }\n\n async updateAuthor(id: string, data: UpdateAuthorInput): Promise<Author> {\n return await this.prisma.author.update({\n where: { id },\n data: {\n ...data,\n updatedAt: new Date(),\n },\n });\n }\n\n async deleteAuthor(id: string): Promise<void> {\n await this.prisma.author.delete({\n where: { id },\n });\n }\n\n async createTag(tag: CreateTagInput): Promise<Tag> {\n return await this.prisma.tag.create({\n data: tag,\n });\n }\n\n async getTag(id: string): Promise<Tag | null> {\n return await this.prisma.tag.findUnique({\n where: { id },\n });\n }\n\n async getTagBySlug(slug: string): Promise<Tag | null> {\n return await this.prisma.tag.findUnique({\n where: { slug },\n });\n }\n\n async listTags(options?: { limit?: number; offset?: number }): Promise<Tag[]> {\n return await this.prisma.tag.findMany({\n orderBy: { name: 'asc' },\n take: options?.limit || 100,\n skip: options?.offset || 0,\n });\n }\n\n async updateTag(id: string, data: UpdateTagInput): Promise<Tag> {\n return await this.prisma.tag.update({\n where: { id },\n data: {\n ...data,\n updatedAt: new Date(),\n },\n });\n }\n\n async deleteTag(id: string): Promise<void> {\n // PostTag rows are cascade-deleted via onDelete: Cascade in schema\n await this.prisma.tag.delete({\n where: { id },\n });\n }\n\n // Slug generation is handled by PostsSDK using src/sdk/utils/slug-generator.ts\n}\n","/**\n * BlogSDK — Main Entry Point\n *\n * Initialize with a database adapter to get full CRUD for posts, authors, and tags.\n *\n * @example\n * const blog = new BlogSDK(new PostgresAdapter(prisma));\n * const post = await blog.posts.create({ title: \"Hello\", content: \"...\", authorId: \"...\" });\n *\n * Ref: docs/epic-blog-lib/02-architecture.md (lines 169-228)\n */\n\nimport type { DatabaseAdapter } from './adapters/base';\nimport { PostsSDK } from './posts';\nimport { AuthorsSDK } from './authors';\nimport { TagsSDK } from './tags';\nimport { GCSStorage } from './storage/gcs';\nimport type { GCSStorageConfig } from './storage/gcs';\n\nexport interface BlogSDKConfig {\n adapter: DatabaseAdapter;\n gcs?: {\n bucket: string;\n projectId: string;\n keyFile?: string;\n /** 'public' (default) or 'private' — private uses signed read URLs. */\n accessMode?: 'public' | 'private';\n /** Signed read URL expiry in seconds (default 3600). Only used when accessMode is 'private'. */\n signedReadUrlExpiresInSeconds?: number;\n };\n}\n\nexport class BlogSDK {\n readonly posts: PostsSDK;\n readonly authors: AuthorsSDK;\n readonly tags: TagsSDK;\n\n private adapter: DatabaseAdapter;\n private gcsStorage?: GCSStorage;\n\n constructor(config: BlogSDKConfig) {\n this.adapter = config.adapter;\n this.posts = new PostsSDK(this.adapter);\n this.authors = new AuthorsSDK(this.adapter);\n this.tags = new TagsSDK(this.adapter);\n\n if (config.gcs) {\n this.gcsStorage = new GCSStorage(config.gcs);\n }\n }\n\n /**\n * Static factory method for initialization.\n * Validates config and returns a ready-to-use BlogSDK instance.\n */\n static async initialize(config: BlogSDKConfig): Promise<BlogSDK> {\n return new BlogSDK(config);\n }\n\n /**\n * Upload an image to GCS. Requires gcs config in BlogSDKConfig.\n * Returns { uploadUrl, publicUrl } — client uploads directly to uploadUrl.\n */\n async getImageUploadUrl(\n filename: string,\n contentType: string,\n options?: { folder?: string },\n ): Promise<{ uploadUrl: string; publicUrl: string }> {\n if (!this.gcsStorage) {\n throw new Error('GCS storage not configured. Pass gcs config to BlogSDK constructor.');\n }\n\n const destinationPath = options?.folder\n ? `${options.folder}/${filename}`\n : filename;\n\n return this.gcsStorage.getPresignedUploadUrl(destinationPath, contentType);\n }\n}\n\n// Re-export SDK classes for direct access\nexport { PostsSDK } from './posts';\nexport { AuthorsSDK } from './authors';\nexport { TagsSDK } from './tags';\nexport { PostgresAdapter } from './adapters/postgres';\nexport type { DatabaseAdapter, AdapterPost } from './adapters/base';\nexport { GCSStorage } from './storage/gcs';\nexport type { GCSStorageConfig } from './storage/gcs';\n"],"mappings":"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","names":[]}
@@ -0,0 +1,122 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defProps = Object.defineProperties;
3
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
7
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
+ var __spreadValues = (a, b) => {
9
+ for (var prop in b || (b = {}))
10
+ if (__hasOwnProp.call(b, prop))
11
+ __defNormalProp(a, prop, b[prop]);
12
+ if (__getOwnPropSymbols)
13
+ for (var prop of __getOwnPropSymbols(b)) {
14
+ if (__propIsEnum.call(b, prop))
15
+ __defNormalProp(a, prop, b[prop]);
16
+ }
17
+ return a;
18
+ };
19
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
20
+ var __objRest = (source, exclude) => {
21
+ var target = {};
22
+ for (var prop in source)
23
+ if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
24
+ target[prop] = source[prop];
25
+ if (source != null && __getOwnPropSymbols)
26
+ for (var prop of __getOwnPropSymbols(source)) {
27
+ if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
28
+ target[prop] = source[prop];
29
+ }
30
+ return target;
31
+ };
32
+
33
+ // src/sdk/storage/gcs.ts
34
+ var GCSStorage = class {
35
+ constructor(config) {
36
+ this.storage = null;
37
+ var _a, _b;
38
+ this.bucketName = config.bucket;
39
+ this.accessMode = (_a = config.accessMode) != null ? _a : "public";
40
+ this.signedReadUrlExpiresInSeconds = (_b = config.signedReadUrlExpiresInSeconds) != null ? _b : 3600;
41
+ this.storageInitPromise = import("@google-cloud/storage").then(({ Storage }) => {
42
+ this.storage = new Storage({
43
+ projectId: config.projectId,
44
+ keyFilename: config.keyFile
45
+ });
46
+ return this.storage;
47
+ }).catch(() => {
48
+ throw new Error(
49
+ "@google-cloud/storage is not installed. Install it to use GCS features: npm install @google-cloud/storage"
50
+ );
51
+ });
52
+ }
53
+ async getStorage() {
54
+ if (this.storage) return this.storage;
55
+ return this.storageInitPromise;
56
+ }
57
+ /**
58
+ * Generate a presigned URL for direct browser upload to GCS.
59
+ *
60
+ * @param filename - Destination path in bucket (e.g. "blog/posts/1234-abc.jpg")
61
+ * @param contentType - MIME type (e.g. "image/jpeg")
62
+ * @param uploadUrlExpiresInSeconds - URL expiry (default 300 = 5 min)
63
+ */
64
+ async getPresignedUploadUrl(filename, contentType, uploadUrlExpiresInSeconds = 300) {
65
+ const storage = await this.getStorage();
66
+ const file = storage.bucket(this.bucketName).file(filename);
67
+ const [uploadUrl] = await file.getSignedUrl({
68
+ version: "v4",
69
+ action: "write",
70
+ expires: Date.now() + uploadUrlExpiresInSeconds * 1e3,
71
+ contentType
72
+ });
73
+ let publicUrl;
74
+ if (this.accessMode === "private") {
75
+ const [signedReadUrl] = await file.getSignedUrl({
76
+ version: "v4",
77
+ action: "read",
78
+ expires: Date.now() + this.signedReadUrlExpiresInSeconds * 1e3
79
+ });
80
+ publicUrl = signedReadUrl;
81
+ } else {
82
+ publicUrl = `https://storage.googleapis.com/${this.bucketName}/${filename}`;
83
+ }
84
+ return { uploadUrl, publicUrl };
85
+ }
86
+ /**
87
+ * Get a read URL for an existing file.
88
+ * Returns a direct public URL or a v4 signed URL depending on accessMode.
89
+ */
90
+ async getReadUrl(filename) {
91
+ if (this.accessMode === "private") {
92
+ const storage = await this.getStorage();
93
+ const file = storage.bucket(this.bucketName).file(filename);
94
+ const [signedReadUrl] = await file.getSignedUrl({
95
+ version: "v4",
96
+ action: "read",
97
+ expires: Date.now() + this.signedReadUrlExpiresInSeconds * 1e3
98
+ });
99
+ return signedReadUrl;
100
+ }
101
+ return `https://storage.googleapis.com/${this.bucketName}/${filename}`;
102
+ }
103
+ /** Delete a file from GCS. */
104
+ async delete(filename) {
105
+ const storage = await this.getStorage();
106
+ await storage.bucket(this.bucketName).file(filename).delete();
107
+ }
108
+ /** Check if a file exists in the bucket. */
109
+ async exists(filename) {
110
+ const storage = await this.getStorage();
111
+ const [fileExists] = await storage.bucket(this.bucketName).file(filename).exists();
112
+ return fileExists;
113
+ }
114
+ };
115
+
116
+ export {
117
+ __spreadValues,
118
+ __spreadProps,
119
+ __objRest,
120
+ GCSStorage
121
+ };
122
+ //# sourceMappingURL=chunk-OPJV2ECE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sdk/storage/gcs.ts"],"sourcesContent":["/**\n * Google Cloud Storage Integration\n *\n * Handles presigned URL generation, file deletion, and existence checks\n * for image uploads to GCS.\n *\n * Ref: docs/epic-blog-lib/02-architecture.md (lines 866-923)\n * Ref: docs/epic-blog-lib/03-tasks.md (Task openclaw-n1y.9)\n */\n\nimport type { Storage as StorageType } from '@google-cloud/storage';\n\nexport interface GCSStorageConfig {\n projectId: string;\n bucket: string;\n keyFile?: string;\n /**\n * 'public' (default) — files are publicly readable via direct URL.\n * 'private' — files require a v4 signed read URL to access.\n *\n * Ref: Plan Phase 5, Step 5.5\n */\n accessMode?: 'public' | 'private';\n /** How long signed read URLs last in seconds (default 3600 = 1 hour). Only used when accessMode is 'private'. */\n signedReadUrlExpiresInSeconds?: number;\n}\n\nexport interface PresignedUploadResult {\n uploadUrl: string;\n /** Direct public URL when accessMode='public', or a v4 signed read URL when accessMode='private'. */\n publicUrl: string;\n}\n\nexport class GCSStorage {\n private storage: StorageType | null = null;\n private storageInitPromise: Promise<StorageType>;\n private bucketName: string;\n private accessMode: 'public' | 'private';\n private signedReadUrlExpiresInSeconds: number;\n\n constructor(config: GCSStorageConfig) {\n this.bucketName = config.bucket;\n this.accessMode = config.accessMode ?? 'public';\n this.signedReadUrlExpiresInSeconds = config.signedReadUrlExpiresInSeconds ?? 3600;\n\n // Lazy-load @google-cloud/storage so the SDK doesn't crash\n // at import time when GCS isn't installed. Ref: Plan blog-6cd\n this.storageInitPromise = import('@google-cloud/storage')\n .then(({ Storage }) => {\n this.storage = new Storage({\n projectId: config.projectId,\n keyFilename: config.keyFile,\n });\n return this.storage;\n })\n .catch(() => {\n throw new Error(\n '@google-cloud/storage is not installed. ' +\n 'Install it to use GCS features: npm install @google-cloud/storage'\n );\n });\n }\n\n private async getStorage(): Promise<StorageType> {\n if (this.storage) return this.storage;\n return this.storageInitPromise;\n }\n\n /**\n * Generate a presigned URL for direct browser upload to GCS.\n *\n * @param filename - Destination path in bucket (e.g. \"blog/posts/1234-abc.jpg\")\n * @param contentType - MIME type (e.g. \"image/jpeg\")\n * @param uploadUrlExpiresInSeconds - URL expiry (default 300 = 5 min)\n */\n async getPresignedUploadUrl(\n filename: string,\n contentType: string,\n uploadUrlExpiresInSeconds: number = 300,\n ): Promise<PresignedUploadResult> {\n const storage = await this.getStorage();\n const file = storage.bucket(this.bucketName).file(filename);\n\n const [uploadUrl] = await file.getSignedUrl({\n version: 'v4',\n action: 'write',\n expires: Date.now() + uploadUrlExpiresInSeconds * 1000,\n contentType,\n });\n\n let publicUrl: string;\n\n if (this.accessMode === 'private') {\n const [signedReadUrl] = await file.getSignedUrl({\n version: 'v4',\n action: 'read',\n expires: Date.now() + this.signedReadUrlExpiresInSeconds * 1000,\n });\n publicUrl = signedReadUrl;\n } else {\n publicUrl = `https://storage.googleapis.com/${this.bucketName}/${filename}`;\n }\n\n return { uploadUrl, publicUrl };\n }\n\n /**\n * Get a read URL for an existing file.\n * Returns a direct public URL or a v4 signed URL depending on accessMode.\n */\n async getReadUrl(filename: string): Promise<string> {\n if (this.accessMode === 'private') {\n const storage = await this.getStorage();\n const file = storage.bucket(this.bucketName).file(filename);\n const [signedReadUrl] = await file.getSignedUrl({\n version: 'v4',\n action: 'read',\n expires: Date.now() + this.signedReadUrlExpiresInSeconds * 1000,\n });\n return signedReadUrl;\n }\n\n return `https://storage.googleapis.com/${this.bucketName}/${filename}`;\n }\n\n /** Delete a file from GCS. */\n async delete(filename: string): Promise<void> {\n const storage = await this.getStorage();\n await storage.bucket(this.bucketName).file(filename).delete();\n }\n\n /** Check if a file exists in the bucket. */\n async exists(filename: string): Promise<boolean> {\n const storage = await this.getStorage();\n const [fileExists] = await storage\n .bucket(this.bucketName)\n .file(filename)\n .exists();\n return fileExists;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCO,IAAM,aAAN,MAAiB;AAAA,EAOtB,YAAY,QAA0B;AANtC,SAAQ,UAA8B;AAlCxC;AAyCI,SAAK,aAAa,OAAO;AACzB,SAAK,cAAa,YAAO,eAAP,YAAqB;AACvC,SAAK,iCAAgC,YAAO,kCAAP,YAAwC;AAI7E,SAAK,qBAAqB,OAAO,uBAAuB,EACrD,KAAK,CAAC,EAAE,QAAQ,MAAM;AACrB,WAAK,UAAU,IAAI,QAAQ;AAAA,QACzB,WAAW,OAAO;AAAA,QAClB,aAAa,OAAO;AAAA,MACtB,CAAC;AACD,aAAO,KAAK;AAAA,IACd,CAAC,EACA,MAAM,MAAM;AACX,YAAM,IAAI;AAAA,QACR;AAAA,MAEF;AAAA,IACF,CAAC;AAAA,EACL;AAAA,EAEA,MAAc,aAAmC;AAC/C,QAAI,KAAK,QAAS,QAAO,KAAK;AAC9B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,sBACJ,UACA,aACA,4BAAoC,KACJ;AAChC,UAAM,UAAU,MAAM,KAAK,WAAW;AACtC,UAAM,OAAO,QAAQ,OAAO,KAAK,UAAU,EAAE,KAAK,QAAQ;AAE1D,UAAM,CAAC,SAAS,IAAI,MAAM,KAAK,aAAa;AAAA,MAC1C,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,SAAS,KAAK,IAAI,IAAI,4BAA4B;AAAA,MAClD;AAAA,IACF,CAAC;AAED,QAAI;AAEJ,QAAI,KAAK,eAAe,WAAW;AACjC,YAAM,CAAC,aAAa,IAAI,MAAM,KAAK,aAAa;AAAA,QAC9C,SAAS;AAAA,QACT,QAAQ;AAAA,QACR,SAAS,KAAK,IAAI,IAAI,KAAK,gCAAgC;AAAA,MAC7D,CAAC;AACD,kBAAY;AAAA,IACd,OAAO;AACL,kBAAY,kCAAkC,KAAK,UAAU,IAAI,QAAQ;AAAA,IAC3E;AAEA,WAAO,EAAE,WAAW,UAAU;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,WAAW,UAAmC;AAClD,QAAI,KAAK,eAAe,WAAW;AACjC,YAAM,UAAU,MAAM,KAAK,WAAW;AACtC,YAAM,OAAO,QAAQ,OAAO,KAAK,UAAU,EAAE,KAAK,QAAQ;AAC1D,YAAM,CAAC,aAAa,IAAI,MAAM,KAAK,aAAa;AAAA,QAC9C,SAAS;AAAA,QACT,QAAQ;AAAA,QACR,SAAS,KAAK,IAAI,IAAI,KAAK,gCAAgC;AAAA,MAC7D,CAAC;AACD,aAAO;AAAA,IACT;AAEA,WAAO,kCAAkC,KAAK,UAAU,IAAI,QAAQ;AAAA,EACtE;AAAA;AAAA,EAGA,MAAM,OAAO,UAAiC;AAC5C,UAAM,UAAU,MAAM,KAAK,WAAW;AACtC,UAAM,QAAQ,OAAO,KAAK,UAAU,EAAE,KAAK,QAAQ,EAAE,OAAO;AAAA,EAC9D;AAAA;AAAA,EAGA,MAAM,OAAO,UAAoC;AAC/C,UAAM,UAAU,MAAM,KAAK,WAAW;AACtC,UAAM,CAAC,UAAU,IAAI,MAAM,QACxB,OAAO,KAAK,UAAU,EACtB,KAAK,QAAQ,EACb,OAAO;AACV,WAAO;AAAA,EACT;AACF;","names":[]}