@everystack/storage 0.2.1 → 0.3.1

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": "@everystack/storage",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "File uploads via S3 + CloudFront CDN with presigned URLs",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "Scalable Technology, Inc. <licensing@scalable.technology>",
@@ -29,6 +29,10 @@
29
29
  "types": "./src/schema.ts",
30
30
  "default": "./src/schema.ts"
31
31
  },
32
+ "./models": {
33
+ "types": "./src/models/index.ts",
34
+ "default": "./src/models/index.ts"
35
+ },
32
36
  "./client": {
33
37
  "types": "./src/client/index.ts",
34
38
  "default": "./src/client/index.ts"
@@ -38,6 +42,9 @@
38
42
  "default": "./src/plugin.ts"
39
43
  }
40
44
  },
45
+ "dependencies": {
46
+ "@everystack/model": "0.3.3"
47
+ },
41
48
  "peerDependencies": {
42
49
  "@aws-sdk/client-s3": "3.1053.0",
43
50
  "@aws-sdk/s3-request-presigner": "3.1053.0",
@@ -76,7 +83,7 @@
76
83
  "react-test-renderer": "19.2.6",
77
84
  "ts-jest": "29.4.9",
78
85
  "typescript": "5.9.3",
79
- "@everystack/server": "0.2.23"
86
+ "@everystack/server": "0.3.1"
80
87
  },
81
88
  "scripts": {
82
89
  "test": "jest",
@@ -0,0 +1,55 @@
1
+ /**
2
+ * dist.blobs — the content-addressed S3 object registry (deduped by sha256).
3
+ *
4
+ * The v3 Model that owns this table's schema AND its authorization. The drizzle
5
+ * runtime table (`../schema`) derives from it, and `db:generate` reads it to emit
6
+ * the table's DDL + RLS + grants — so the package, not the consuming app, owns
7
+ * dist.blobs end to end.
8
+ *
9
+ * Authz shape (matches the live 0020 migration):
10
+ * - `writtenBy: 'worker'` — the image worker writes derived blobs (thumbnail/webp)
11
+ * as the table owner and must keep its owner bypass, so RLS is ENABLE-not-FORCE.
12
+ * - `can('manage', { role: 'admin' })` — the storage handler runs as `admin`
13
+ * (SET ROLE) and enforces per-user ownership in code; admin gets CRUD + the
14
+ * `blobs_admin` policy. No anon/authenticated grant exists.
15
+ * - `private: true` — reached through the storage handler (/api/storage), never the
16
+ * generic data API, so it never enters `exposedTables`.
17
+ */
18
+
19
+ import { defineModel, field, can, index } from '@everystack/model';
20
+
21
+ export const Blob = defineModel('blobs', {
22
+ schema: 'dist',
23
+ writtenBy: 'worker',
24
+ private: true,
25
+ fields: {
26
+ id: field.uuid().primaryKey().defaultRandom(),
27
+ repositoryId: field.uuid().nullable(),
28
+ type: field.text().notNull(), // 'original' | 'thumbnail' | 'preview' | 'webp' | …
29
+ sha256: field.text().notNull().unique(),
30
+
31
+ // S3 object location (bytes live in S3, not the database)
32
+ s3Key: field.text().nullable(),
33
+ bucket: field.text().nullable(),
34
+
35
+ sizeBytes: field.bigint().notNull(),
36
+ contentType: field.text().nullable(),
37
+
38
+ // Derived content (thumbnail → original, webp → original, …)
39
+ parentId: field.uuid().nullable(),
40
+
41
+ // Intrinsic content metadata ({ width, height, format, originalName, userId })
42
+ metadata: field.jsonb().nullable(),
43
+
44
+ createdAt: field.timestamptz().defaultNow().notNull(),
45
+ },
46
+ constraints: [
47
+ index('type'),
48
+ index('contentType'),
49
+ index('createdAt'),
50
+ index('repositoryId'),
51
+ index('parentId'),
52
+ index('s3Key'),
53
+ ],
54
+ abilities: [can('manage', { role: 'admin' })],
55
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Storage's package-owned models. A consuming app composes these into its own
3
+ * model list so `db:generate` emits dist.blobs' schema + authz alongside the app's
4
+ * tables: `models = [...appModels, ...storageModels]`.
5
+ */
6
+
7
+ export { Blob } from './blob';
8
+
9
+ import { defineModule } from '@everystack/model';
10
+ import { Blob } from './blob';
11
+
12
+ /** All storage-owned models, for the app to spread into its db:generate model list. */
13
+ export const storageModels = [Blob];
14
+
15
+ /** The storage Module — dist.blobs (schema + authz). No functions/extensions of its own. */
16
+ export const storageModule = defineModule({ models: storageModels });
package/src/schema.ts CHANGED
@@ -1,87 +1,15 @@
1
1
  /**
2
- * Storage schema — everyschema dist.blobs
2
+ * Storage schema — dist.blobs, the runtime drizzle table.
3
3
  *
4
- * Content-addressed storage — in-DB (bytea) or S3-backed.
5
- * Replaces the old `uploads` table.
6
- *
7
- * Type values for everystack:
8
- * 'original' — user-uploaded content (S3-backed)
9
- * 'thumbnail' — derived image variant (parentId → original)
10
- * 'preview' — derived preview image
11
- * 'webp' — derived WebP variant
12
- *
13
- * @see everyschema — canonical schema definitions
14
- */
15
-
16
- import {
17
- pgSchema,
18
- text,
19
- timestamp,
20
- uuid,
21
- bigint,
22
- index,
23
- uniqueIndex,
24
- jsonb,
25
- customType,
26
- } from 'drizzle-orm/pg-core';
27
-
28
- const distSchema = pgSchema('dist');
29
-
30
- const bytea = customType<{ data: Buffer; driverValue: Buffer }>({
31
- dataType() {
32
- return 'bytea';
33
- },
34
- });
35
-
36
- // =============================================================================
37
- // BLOBS - Raw binary storage, deduped by sha256
38
- // =============================================================================
39
-
40
- /**
41
- * Content-addressed storage — in-DB or S3-backed.
42
- *
43
- * Storage modes:
44
- * In-DB: data (bytea) populated, s3Key/bucket NULL
45
- * S3: s3Key + bucket populated, data NULL
46
- *
47
- * Derived content uses parentId:
48
- * original → thumbnail, preview, webp, etc.
4
+ * DERIVED from the package-owned Model (`./models/blob`) — the Model is the single
5
+ * source of the table's columns, schema, indexes, and authz; this just materializes
6
+ * the drizzle-orm runtime object the handler queries. Edit the Model, not this file.
49
7
  *
50
- * metadata stores intrinsic content properties:
51
- * Images: { width, height, format, originalName, userId }
52
- * Video/audio: { codec, duration, bitrate }
8
+ * Content-addressed storage — S3-backed (the object registry; bytes live in S3).
9
+ * Replaces the old `uploads` table.
53
10
  */
54
- export const blobs = distSchema.table(
55
- 'blobs',
56
- {
57
- id: uuid('id').primaryKey().defaultRandom(),
58
- repositoryId: uuid('repository_id'),
59
- type: text('type').notNull(), // 'original' | 'thumbnail' | 'preview' | 'webp' | etc.
60
- sha256: text('sha256').notNull().unique(),
61
-
62
- // Storage: in-DB (bytea) or S3-backed (s3Key + bucket)
63
- data: bytea('data'),
64
- s3Key: text('s3_key'),
65
- bucket: text('bucket'),
66
-
67
- sizeBytes: bigint('size_bytes', { mode: 'number' }).notNull(),
68
- contentType: text('content_type'),
69
-
70
- // Derived content (thumbnail → original, webp → original, etc.)
71
- parentId: uuid('parent_id'),
72
11
 
73
- // Intrinsic content metadata
74
- metadata: jsonb('metadata'),
12
+ import { toDrizzleTable } from '@everystack/model/drizzle';
13
+ import { Blob } from './models/blob';
75
14
 
76
- createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
77
- },
78
- (table) => ({
79
- typeIdx: index('blobs_type_idx').on(table.type),
80
- sha256Idx: uniqueIndex('blobs_sha256_idx').on(table.sha256),
81
- contentTypeIdx: index('blobs_content_type_idx').on(table.contentType),
82
- createdAtIdx: index('blobs_created_at_idx').on(table.createdAt),
83
- repoIdx: index('blobs_repo_idx').on(table.repositoryId),
84
- parentIdx: index('blobs_parent_idx').on(table.parentId),
85
- s3KeyIdx: index('blobs_s3_key_idx').on(table.s3Key),
86
- })
87
- );
15
+ export const blobs = toDrizzleTable(Blob);