@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 +9 -2
- package/src/models/blob.ts +55 -0
- package/src/models/index.ts +16 -0
- package/src/schema.ts +9 -81
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@everystack/storage",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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 —
|
|
2
|
+
* Storage schema — dist.blobs, the runtime drizzle table.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
51
|
-
*
|
|
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
|
-
|
|
74
|
-
|
|
12
|
+
import { toDrizzleTable } from '@everystack/model/drizzle';
|
|
13
|
+
import { Blob } from './models/blob';
|
|
75
14
|
|
|
76
|
-
|
|
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);
|