bunderstack 0.11.0 → 0.13.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunderstack",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Batteries-included backend framework for Bun: CRUD APIs, auth, file storage, realtime, tRPC, email, and validated env from a single Drizzle schema and config object.",
5
5
  "keywords": [
6
6
  "backend",
package/src/index.ts CHANGED
@@ -58,7 +58,7 @@ import {
58
58
  import { createRealtimeBroker, buildRealtimeRouter } from './realtime/index'
59
59
  import { createRedisRealtimeBroker } from './realtime/redis'
60
60
  import { deleteFileWithDerivatives } from './storage/delete'
61
- import { deleteFileMetaRow } from './storage/file-meta'
61
+ import { deleteFileMetaRow, insertReadyFile } from './storage/file-meta'
62
62
  import { createBucketStorages } from './storage/registry'
63
63
  import { buildBucketStorageRouter } from './storage/router'
64
64
  import { sweepOrphans } from './storage/sweep'
@@ -115,6 +115,17 @@ export interface StorageFacade {
115
115
  key: string,
116
116
  opts?: { bucket?: string; expiresIn?: number },
117
117
  ): Promise<string>
118
+ /**
119
+ * Upload bytes as a ready file and register it in file_meta so it is
120
+ * accessible via `GET /api/files/<key>`. Use this for server-generated
121
+ * files (e.g. generated PDFs) that are not uploaded by end-users.
122
+ */
123
+ upload(
124
+ key: string,
125
+ body: ArrayBuffer | Uint8Array,
126
+ contentType: string,
127
+ opts?: { ownerId?: string; filename?: string },
128
+ ): Promise<void>
118
129
  }
119
130
 
120
131
  export type AppStartWorkerOptions = Omit<StartWorkerOptions, 'tick'>
@@ -478,7 +489,7 @@ export async function createBunderstack<
478
489
  const bucketName =
479
490
  opts.bucket ??
480
491
  key.split('/')[0] ??
481
- registry.defaultBucketName
492
+ config.storage.defaultBucket
482
493
  const adapter = registry.get(bucketName)?.adapter
483
494
  if (adapter?.presignGet) {
484
495
  return adapter.presignGet(key, {
@@ -487,6 +498,23 @@ export async function createBunderstack<
487
498
  }
488
499
  return `/api/files/${key}`
489
500
  },
501
+ async upload(key, body, contentType, opts = {}) {
502
+ const bucketName = key.split('/')[0] ?? ''
503
+ const adapter = registry.get(bucketName)?.adapter
504
+ if (!adapter) throw new Error(`Unknown bucket: ${bucketName}`)
505
+ const u8 = body instanceof Uint8Array ? body : new Uint8Array(body)
506
+ const buf: ArrayBuffer = u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer
507
+ await adapter.upload(key, buf, contentType)
508
+ await insertReadyFile(db, {
509
+ fileId: key,
510
+ bucket: bucketName,
511
+ ownerId: opts.ownerId ?? null,
512
+ scopeJson: null,
513
+ filename: opts.filename ?? key.split('/').at(-1) ?? null,
514
+ contentType,
515
+ size: buf.byteLength,
516
+ })
517
+ },
490
518
  }
491
519
  const jobRunner = jobsDefs
492
520
  ? createJobRunner({
@@ -613,6 +641,7 @@ export async function createBunderstack<
613
641
  email,
614
642
  jobs,
615
643
  realtime,
644
+ storage,
616
645
  req,
617
646
  }),
618
647
  })
@@ -371,14 +371,16 @@ export function buildBucketStorageRouter(
371
371
  return c.json({ fileId, url }, 200)
372
372
  })
373
373
 
374
- // ─── GET /:bucket/:id ─────────────────────────────────────────────────────
375
- router.get('/:bucket/:id', async (c) => {
374
+ // ─── GET /:bucket/* ──────────────────────────────────────────────────────
375
+ // NOTE: wildcard captures nested paths like `adaptations/uuid/resume.pdf`.
376
+ router.get('/:bucket/*', async (c) => {
376
377
  const bucketName = c.req.param('bucket')
377
378
  const entry = registry.get(bucketName)
378
379
  if (!entry) return apiError(c, ErrorCode.NOT_FOUND, 'Unknown bucket', 404)
379
380
  const { bucket, adapter } = entry
380
381
 
381
- const id = c.req.param('id')
382
+ const mountPrefix = c.req.routePath.replace(/\/:[^\/]+\/\*$/, '')
383
+ const id = c.req.path.slice(`${mountPrefix}/${bucketName}/`.length)
382
384
  const fileId = `${bucketName}/${id}`
383
385
 
384
386
  const row = await getFileMeta(db, fileId)
@@ -467,14 +469,15 @@ export function buildBucketStorageRouter(
467
469
  return new Response(res.body, { status: res.status, headers })
468
470
  })
469
471
 
470
- // ─── DELETE /:bucket/:id ──────────────────────────────────────────────────
471
- router.delete('/:bucket/:id', async (c) => {
472
+ // ─── DELETE /:bucket/* ─────────────────────────────────────────────────
473
+ router.delete('/:bucket/*', async (c) => {
472
474
  const bucketName = c.req.param('bucket')
473
475
  const entry = registry.get(bucketName)
474
476
  if (!entry) return apiError(c, ErrorCode.NOT_FOUND, 'Unknown bucket', 404)
475
477
  const { bucket, adapter } = entry
476
478
 
477
- const id = c.req.param('id')
479
+ const mountPrefix = c.req.routePath.replace(/\/:[^/]+\/\*$/, '')
480
+ const id = c.req.path.slice(`${mountPrefix}/${bucketName}/`.length)
478
481
  const fileId = `${bucketName}/${id}`
479
482
 
480
483
  const row = await getFileMeta(db, fileId)
package/src/trpc.ts CHANGED
@@ -7,6 +7,7 @@ import type { DbFor } from './db'
7
7
  import type { EmailFacade } from './email'
8
8
  import type { JobsRuntimeFacade } from './jobs/index'
9
9
  import type { RealtimeFacade } from './realtime/facade'
10
+ import type { StorageFacade } from './index'
10
11
 
11
12
  export type TRPCContext<
12
13
  TSchema extends Record<string, unknown>,
@@ -18,6 +19,7 @@ export type TRPCContext<
18
19
  email: EmailFacade
19
20
  jobs: JobsRuntimeFacade
20
21
  realtime: RealtimeFacade<TSchema>
22
+ storage: StorageFacade
21
23
  req: Request
22
24
  }
23
25