@fabrika/db 0.5.5 → 0.7.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.
@@ -1,133 +0,0 @@
1
- // The names and the DDL of a file collection's table (`fabrika_files_<collection>`). Pure: `@fabrika/db/framework`
2
- // re-exports `fileTableName` for `boot`, which the Worker adapter imports, and an application's migration uses the DDL.
3
-
4
- import {
5
- FILE_SCAN_COLUMNS,
6
- FILE_STATUSES,
7
- FILE_TABLE_PREFIX,
8
- fileScanAlter,
9
- fileScanIndex,
10
- fileStatusCheck,
11
- legacyStatusCheckName,
12
- } from '@fabrika/platform-node/sql'
13
- import { QueryRefusedError } from './errors'
14
- import { isPlainIdentifier } from './identifier'
15
-
16
- export { FILE_STATUSES, FILE_TABLE_PREFIX, legacyStatusCheckName }
17
-
18
- /** `<table>_removable`, the longest name the DDL derives, must stay within Postgres' 63 characters. */
19
- const MAX_COLLECTION_LENGTH = 63 - FILE_TABLE_PREFIX.length - '_removable'.length
20
-
21
- /** The columns every file row has; an owner's containment column may not take one of these names. */
22
- export const FILE_COLUMNS: readonly string[] = [
23
- 'id',
24
- 'owner_id',
25
- 'status',
26
- 'object_key',
27
- 'size',
28
- 'content_type',
29
- 'sha256',
30
- 'original_name',
31
- 'created_by',
32
- 'created_at',
33
- 'deleted_at',
34
- 'object_version',
35
- 'scan_attempts',
36
- 'scan_after',
37
- ]
38
-
39
- export type FileStatus = (typeof FILE_STATUSES)[number]
40
-
41
- /** The column types whose values `Bun.SQL` decodes as strings, as a capability carries them. */
42
- export type FileColumnType = 'text' | 'uuid'
43
-
44
- export interface FileColumn {
45
- readonly column: string
46
- readonly type: FileColumnType
47
- }
48
-
49
- export interface FileOwner {
50
- readonly table: string
51
- readonly key: FileColumn
52
- /** The owner's containment columns, as its table policy declares them, nearest container first. */
53
- readonly chain: readonly FileColumn[]
54
- }
55
-
56
- /** `fabrika_files_<collection>`, or `null` when the collection's name cannot name a table. */
57
- export function fileTableName(collection: string): string | null {
58
- const table = `${FILE_TABLE_PREFIX}${collection}`
59
- return collection.length <= MAX_COLLECTION_LENGTH && isPlainIdentifier(collection) && isPlainIdentifier(table) ? table : null
60
- }
61
-
62
- export function tableOf(collection: string): string {
63
- const table = fileTableName(collection)
64
- if (table === null) {
65
- throw new QueryRefusedError(
66
- 'invalid_name',
67
- `the file collection '${collection}' needs a lowercase snake_case name of at most ${MAX_COLLECTION_LENGTH} characters`,
68
- )
69
- }
70
- return table
71
- }
72
-
73
- function sqlType(column: FileColumn): string {
74
- if (!isPlainIdentifier(column.column)) {
75
- throw new QueryRefusedError('invalid_name', 'an owner column must match ^[a-z_][a-z0-9_]*$')
76
- }
77
- if (column.type !== 'text' && column.type !== 'uuid') {
78
- throw new QueryRefusedError('invalid_name', "an owner column's type must be 'text' or 'uuid'")
79
- }
80
- return column.type.toUpperCase()
81
- }
82
-
83
- /**
84
- * The migration that creates a collection's table. A row carries its owner's key and the owner's whole containment
85
- * chain under the owner's column names, with a composite foreign key: a move of the owner cascades into its files,
86
- * so a capability minted before the move matches none of them, and deleting the owner orphans them (every owner
87
- * column becomes null) for the sweep. The owner table needs `UNIQUE (<key>, <chain columns>)`, as a child table's
88
- * parent does. A row is `pending` from before its object is written until the write completed.
89
- */
90
- export function fileTableSql(collection: string, owner: FileOwner): string {
91
- const table = tableOf(collection)
92
- if (!isPlainIdentifier(owner.table)) {
93
- throw new QueryRefusedError('invalid_name', 'the owner table must match ^[a-z_][a-z0-9_]*$')
94
- }
95
- const keyType = sqlType(owner.key)
96
- const chain = owner.chain.map((link) => ({ column: link.column, type: sqlType(link) }))
97
- const taken = chain.find((link) => FILE_COLUMNS.includes(link.column))
98
- if (taken !== undefined) {
99
- throw new QueryRefusedError('invalid_name', `the owner column ${taken.column} has the name of a file column (${FILE_COLUMNS.join(', ')})`)
100
- }
101
- const fileColumns = ['owner_id', ...chain.map((link) => link.column)].join(', ')
102
- const ownerColumns = [owner.key.column, ...chain.map((link) => link.column)].join(', ')
103
- return `CREATE TABLE ${table} (
104
- id UUID PRIMARY KEY,
105
- owner_id ${keyType},
106
- ${chain.map((link) => `\t${link.column} ${link.type},\n`).join('')} status TEXT NOT NULL ${fileStatusCheck(table)},
107
- object_key TEXT NOT NULL UNIQUE,
108
- size BIGINT,
109
- content_type TEXT NOT NULL,
110
- sha256 TEXT,
111
- original_name TEXT,
112
- created_by TEXT NOT NULL,
113
- created_at TIMESTAMPTZ NOT NULL,
114
- deleted_at TIMESTAMPTZ,
115
- ${
116
- FILE_SCAN_COLUMNS.map((column) => `\t${column},\n`).join('')
117
- } FOREIGN KEY (${fileColumns}) REFERENCES ${owner.table} (${ownerColumns}) ON UPDATE CASCADE ON DELETE SET NULL
118
- );
119
- CREATE INDEX ${table}_owner ON ${table} (owner_id) WHERE deleted_at IS NULL;
120
- CREATE INDEX ${table}_removable ON ${table} (created_at) WHERE deleted_at IS NOT NULL OR owner_id IS NULL OR status = 'pending';
121
- ${fileScanIndex(table)};
122
- `
123
- }
124
-
125
- /**
126
- * The migration that lets an existing collection turn `scan: true` on: it replaces the status check of a table an
127
- * earlier `fileTableSql` created, which admitted only `pending` and `live`, by the check of every state, and adds the
128
- * scan job's columns and index. A table `fileTableSql` creates now needs none of it.
129
- */
130
- export function fileScanSql(collection: string): string {
131
- const table = tableOf(collection)
132
- return `${fileScanAlter(table)};\n${fileScanIndex(table)};\n`
133
- }