bunderstack 0.1.0 → 0.3.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.
@@ -27,21 +27,20 @@
27
27
  * See docs/plans/2026-06-28-multi-bucket-storage-design.md §6 for full rationale.
28
28
  */
29
29
 
30
- import type { LibSQLDatabase } from 'drizzle-orm/libsql'
31
-
32
30
  import { eq, and, lt, sql } from 'drizzle-orm'
33
31
 
32
+ import type { AnyDb } from '../dialect'
34
33
  import type { ScopeMap } from '../access'
35
34
 
36
35
  import { rowMatchesScope } from '../access'
37
- import { bunderstackFiles } from '../internal-tables'
36
+ import { bunderstackFiles, filesTableFor } from '../internal-tables'
38
37
 
39
38
  export type FileMetaRow = typeof bunderstackFiles.$inferSelect
40
39
 
41
40
  // ─── CRUD ─────────────────────────────────────────────────────────────────────
42
41
 
43
42
  export async function insertPendingFile(
44
- db: LibSQLDatabase<Record<string, unknown>>,
43
+ db: AnyDb,
45
44
  input: {
46
45
  fileId: string
47
46
  bucket: string
@@ -51,7 +50,8 @@ export async function insertPendingFile(
51
50
  contentType: string | null
52
51
  },
53
52
  ): Promise<void> {
54
- await db.insert(bunderstackFiles).values({
53
+ const files = filesTableFor(db)
54
+ await db.insert(files).values({
55
55
  fileId: input.fileId,
56
56
  bucket: input.bucket,
57
57
  ownerId: input.ownerId,
@@ -66,7 +66,7 @@ export async function insertPendingFile(
66
66
  }
67
67
 
68
68
  export async function insertReadyFile(
69
- db: LibSQLDatabase<Record<string, unknown>>,
69
+ db: AnyDb,
70
70
  input: {
71
71
  fileId: string
72
72
  bucket: string
@@ -77,8 +77,9 @@ export async function insertReadyFile(
77
77
  size: number | null
78
78
  },
79
79
  ): Promise<void> {
80
+ const files = filesTableFor(db)
80
81
  const now = Date.now()
81
- await db.insert(bunderstackFiles).values({
82
+ await db.insert(files).values({
82
83
  fileId: input.fileId,
83
84
  bucket: input.bucket,
84
85
  ownerId: input.ownerId,
@@ -93,79 +94,76 @@ export async function insertReadyFile(
93
94
  }
94
95
 
95
96
  export async function markFileReady(
96
- db: LibSQLDatabase<Record<string, unknown>>,
97
+ db: AnyDb,
97
98
  fileId: string,
98
99
  patch: { size: number | null; contentType: string | null },
99
100
  ): Promise<void> {
101
+ const files = filesTableFor(db)
100
102
  await db
101
- .update(bunderstackFiles)
103
+ .update(files)
102
104
  .set({
103
105
  status: 'ready',
104
106
  confirmedAt: Date.now(),
105
107
  size: patch.size,
106
108
  contentType: patch.contentType,
107
109
  })
108
- .where(eq(bunderstackFiles.fileId, fileId))
110
+ .where(eq(files.fileId, fileId))
109
111
  }
110
112
 
111
113
  export async function getFileMeta(
112
- db: LibSQLDatabase<Record<string, unknown>>,
114
+ db: AnyDb,
113
115
  fileId: string,
114
116
  ): Promise<FileMetaRow | null> {
117
+ const files = filesTableFor(db)
115
118
  const rows = await db
116
119
  .select()
117
- .from(bunderstackFiles)
118
- .where(eq(bunderstackFiles.fileId, fileId))
120
+ .from(files)
121
+ .where(eq(files.fileId, fileId))
119
122
  .limit(1)
120
- return rows[0] ?? null
123
+ return (rows[0] as FileMetaRow | undefined) ?? null
121
124
  }
122
125
 
123
126
  export async function deleteFileMetaRow(
124
- db: LibSQLDatabase<Record<string, unknown>>,
127
+ db: AnyDb,
125
128
  fileId: string,
126
129
  ): Promise<void> {
127
- await db.delete(bunderstackFiles).where(eq(bunderstackFiles.fileId, fileId))
130
+ const files = filesTableFor(db)
131
+ await db.delete(files).where(eq(files.fileId, fileId))
128
132
  }
129
133
 
130
134
  // ─── Sweep ────────────────────────────────────────────────────────────────────
131
135
 
132
136
  export async function listStalePendingFiles(
133
- db: LibSQLDatabase<Record<string, unknown>>,
137
+ db: AnyDb,
134
138
  olderThanMs: number,
135
139
  ): Promise<FileMetaRow[]> {
140
+ const files = filesTableFor(db)
136
141
  return db
137
142
  .select()
138
- .from(bunderstackFiles)
139
- .where(
140
- and(
141
- eq(bunderstackFiles.status, 'pending'),
142
- lt(bunderstackFiles.createdAt, olderThanMs),
143
- ),
144
- )
143
+ .from(files)
144
+ .where(and(eq(files.status, 'pending'), lt(files.createdAt, olderThanMs)))
145
145
  }
146
146
 
147
147
  // ─── Quota ────────────────────────────────────────────────────────────────────
148
148
 
149
149
  export async function sumReadySize(
150
- db: LibSQLDatabase<Record<string, unknown>>,
150
+ db: AnyDb,
151
151
  q: { bucket: string; ownerId?: string; scopeJson?: string },
152
152
  ): Promise<number> {
153
- const conditions = [
154
- eq(bunderstackFiles.status, 'ready'),
155
- eq(bunderstackFiles.bucket, q.bucket),
156
- ]
153
+ const files = filesTableFor(db)
154
+ const conditions = [eq(files.status, 'ready'), eq(files.bucket, q.bucket)]
157
155
  if (q.ownerId !== undefined) {
158
- conditions.push(eq(bunderstackFiles.ownerId, q.ownerId))
156
+ conditions.push(eq(files.ownerId, q.ownerId))
159
157
  }
160
158
  if (q.scopeJson !== undefined) {
161
- conditions.push(eq(bunderstackFiles.scopeJson, q.scopeJson))
159
+ conditions.push(eq(files.scopeJson, q.scopeJson))
162
160
  }
163
161
 
164
162
  const rows = await db
165
163
  .select({
166
- total: sql<number>`coalesce(sum(${bunderstackFiles.size}), 0)`,
164
+ total: sql<number>`coalesce(sum(${files.size}), 0)`,
167
165
  })
168
- .from(bunderstackFiles)
166
+ .from(files)
169
167
  .where(and(...conditions))
170
168
 
171
169
  const raw = rows[0]?.total ?? 0
@@ -1,4 +1,3 @@
1
- import type { LibSQLDatabase } from 'drizzle-orm/libsql'
2
1
  // src/storage/router.ts
3
2
  import type { Context } from 'hono'
4
3
 
@@ -11,6 +10,7 @@ import type {
11
10
  AuthSessionResolver,
12
11
  OperationRule,
13
12
  } from '../access'
13
+ import type { AnyDb } from '../dialect'
14
14
  import type { BucketStorageRegistry } from './registry'
15
15
 
16
16
  import { checkAccess, resolveSession } from '../access'
@@ -35,7 +35,7 @@ import {
35
35
 
36
36
  export interface BucketStorageRouterOptions {
37
37
  registry: BucketStorageRegistry
38
- db: LibSQLDatabase<Record<string, unknown>>
38
+ db: AnyDb
39
39
  auth: AuthSessionResolver | undefined
40
40
  /** Default presign TTL (seconds) for PUT/GET URLs. */
41
41
  presignExpiresSec?: number
@@ -511,7 +511,7 @@ export function buildBucketStorageRouter(
511
511
  * dimension (perUser uses ownerId; perScope uses scopeJson).
512
512
  */
513
513
  async function quotaExceeded(
514
- db: LibSQLDatabase<Record<string, unknown>>,
514
+ db: AnyDb,
515
515
  bucket: string,
516
516
  quota: { perUserBytes?: number; perScopeBytes?: number },
517
517
  ownerId: string | undefined,
@@ -1,6 +1,5 @@
1
1
  // src/storage/sweep.ts
2
- import type { LibSQLDatabase } from 'drizzle-orm/libsql'
3
-
2
+ import type { AnyDb } from '../dialect'
4
3
  import type { BucketStorageRegistry } from './registry'
5
4
 
6
5
  import { deleteFileMetaRow, listStalePendingFiles } from './file-meta'
@@ -12,7 +11,7 @@ import { deleteFileMetaRow, listStalePendingFiles } from './file-meta'
12
11
  */
13
12
  export async function sweepOrphans(
14
13
  registry: BucketStorageRegistry,
15
- db: LibSQLDatabase<Record<string, unknown>>,
14
+ db: AnyDb,
16
15
  olderThanMs: number,
17
16
  ): Promise<number> {
18
17
  const cutoff = Date.now() - olderThanMs
package/src/trpc.ts CHANGED
@@ -1,17 +1,16 @@
1
1
  // src/trpc.ts — pre-wired tRPC instance for bunderstack endpoints.
2
- import type { LibSQLDatabase } from 'drizzle-orm/libsql'
3
-
4
2
  import { initTRPC, TRPCError } from '@trpc/server'
5
3
  import superjson from 'superjson'
6
4
 
7
5
  import type { AccessUser } from './access'
6
+ import type { DbFor } from './db'
8
7
  import type { EmailFacade } from './email'
9
8
 
10
9
  export type TRPCContext<
11
10
  TSchema extends Record<string, unknown>,
12
11
  TEnvResult = Record<string, unknown>,
13
12
  > = {
14
- db: LibSQLDatabase<TSchema>
13
+ db: DbFor<TSchema>
15
14
  user: AccessUser | null
16
15
  env: TEnvResult
17
16
  email: EmailFacade
@@ -0,0 +1,24 @@
1
+ // src/typeid-pg.ts — Postgres twin of the typeid column builder. The codec
2
+ // (generate/parse/encode/decode) is dialect-neutral and lives in ./typeid;
3
+ // only the drizzle customType wrapper differs.
4
+ import { customType } from 'drizzle-orm/pg-core'
5
+
6
+ import { isValidPrefix, type TypeId } from './typeid'
7
+
8
+ /**
9
+ * Drizzle column builder for a branded TypeID text value (Postgres). Stores a
10
+ * plain `text` column so drizzle-kit migrations and `$inferSelect` work
11
+ * unchanged.
12
+ *
13
+ * id: typeid('post').primaryKey().$defaultFn(() => generate('post'))
14
+ */
15
+ export function typeid<P extends string>(prefix: P) {
16
+ if (!isValidPrefix(prefix))
17
+ throw new Error(`Invalid typeid prefix: "${prefix}"`)
18
+ return customType<{ data: TypeId<P>; driverData: string }>({
19
+ dataType: () => 'text',
20
+ })()
21
+ }
22
+
23
+ export { generate, parse, asTypeId, encode, decode } from './typeid'
24
+ export type { TypeId } from './typeid'