@fabrika/platform-node 0.6.0 → 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.
- package/package.json +2 -2
- package/src/file-table-ddl.ts +17 -5
- package/src/migration-classify.ts +53 -8
- package/src/sql.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabrika/platform-node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -37,6 +37,6 @@
|
|
|
37
37
|
"test": "bun test"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@fabrika/platform": "0.
|
|
40
|
+
"@fabrika/platform": "0.7.0"
|
|
41
41
|
}
|
|
42
42
|
}
|
package/src/file-table-ddl.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// The SQL of a file collection's table
|
|
2
|
-
// `
|
|
3
|
-
//
|
|
1
|
+
// The SQL of a file collection's table, which an application on an older framework release (up to 0.5) created with
|
|
2
|
+
// that release's `fileTableSql` and widened with its `fileScanSql`. The migration classifier admits exactly those
|
|
3
|
+
// statements and an adoption migration's calls of the framework's copy and release functions. Pure: tokens in, answers out.
|
|
4
4
|
|
|
5
5
|
import { type SqlToken, sqlTokens, topLevelStatements, upperWord } from './migration-sql'
|
|
6
6
|
import { nameText } from './sql-names'
|
|
@@ -13,6 +13,18 @@ export const FILE_STATUSES = ['pending', 'live', 'quarantined', 'rejected'] as c
|
|
|
13
13
|
|
|
14
14
|
export const FILE_TABLE_PREFIX = 'fabrika_files_'
|
|
15
15
|
|
|
16
|
+
/** Every stored file (`@fabrika/db`'s `0008_files.sql`): an application's file column is a foreign key to its `id`. */
|
|
17
|
+
export const STORED_FILES_TABLE = 'fabrika_files'
|
|
18
|
+
|
|
19
|
+
/** What an application's last adoption migration of a file collection calls, with the collection's name. */
|
|
20
|
+
export const FILES_RELEASE_FUNCTION = 'fabrika_files_release'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* What an adoption migration calls right before it reads a collection's table: it copies the rows an older instance
|
|
24
|
+
* wrote after `0008_files.sql` ran, so every row the migration adopts has its `fabrika_files` row.
|
|
25
|
+
*/
|
|
26
|
+
export const FILES_COPY_FUNCTION = 'fabrika_files_copy_legacy'
|
|
27
|
+
|
|
16
28
|
/** The columns the scan job keeps: the scanned object's version, and the retry count and time of a file without a verdict. */
|
|
17
29
|
export const FILE_SCAN_COLUMNS = ['object_version TEXT', 'scan_attempts INTEGER NOT NULL DEFAULT 0', 'scan_after TIMESTAMPTZ'] as const
|
|
18
30
|
|
|
@@ -29,14 +41,14 @@ export function fileScanIndex(table: string): string {
|
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
/**
|
|
32
|
-
* The name Postgres gave the unnamed status check of a table an
|
|
44
|
+
* The name Postgres gave the unnamed status check of a table an older release's `fileTableSql` created:
|
|
33
45
|
* `<table>_status_check`, the table's name cut to 50 characters first (`makeObjectName`).
|
|
34
46
|
*/
|
|
35
47
|
export function legacyStatusCheckName(table: string): string {
|
|
36
48
|
return `${table.slice(0, 63 - '_status_check'.length)}_status_check`
|
|
37
49
|
}
|
|
38
50
|
|
|
39
|
-
/** The statement of `fileScanSql` that widens
|
|
51
|
+
/** The statement of an older release's `fileScanSql` that widens its collection table: its check and its columns. */
|
|
40
52
|
export function fileScanAlter(table: string): string {
|
|
41
53
|
const columns = FILE_SCAN_COLUMNS.map((column) => `ADD COLUMN ${column}`).join(', ')
|
|
42
54
|
return `ALTER TABLE ${table} DROP CONSTRAINT IF EXISTS ${legacyStatusCheckName(table)}, ADD ${fileStatusCheck(table)}, ${columns}`
|
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
// does not recognise: an allowlist at every level, so a form a later Postgres adds is refused, not executed.
|
|
4
4
|
// Runtime-neutral: Control classifies a commit's migrations in a Worker, the runner before it applies them.
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
FILE_TABLE_PREFIX,
|
|
8
|
+
FILES_COPY_FUNCTION,
|
|
9
|
+
FILES_RELEASE_FUNCTION,
|
|
10
|
+
fileScanAlterTable,
|
|
11
|
+
legacyStatusCheckName,
|
|
12
|
+
STORED_FILES_TABLE,
|
|
13
|
+
} from './file-table-ddl'
|
|
7
14
|
import { lexSql, type SqlToken, topLevelStatements, upperWord } from './migration-sql'
|
|
8
15
|
import { PG_CATALOG_FUNCTIONS, SQL_KEYWORDS } from './pg-catalog-names'
|
|
9
16
|
import {
|
|
@@ -13,6 +20,7 @@ import {
|
|
|
13
20
|
dmlForeignSchema,
|
|
14
21
|
frameworkReference,
|
|
15
22
|
functionUse,
|
|
23
|
+
isFrameworkName,
|
|
16
24
|
isName,
|
|
17
25
|
isPunct,
|
|
18
26
|
nameReferences,
|
|
@@ -328,7 +336,9 @@ function literalBody(token: SqlToken | undefined): SqlToken[] | undefined {
|
|
|
328
336
|
function checkBody(tokens: readonly SqlToken[], context: Context): void {
|
|
329
337
|
const unicode = unicodeIdentifier(tokens)
|
|
330
338
|
if (unicode !== undefined) throw new Refused('a Unicode-escaped identifier (U&"…")', unicode.line)
|
|
331
|
-
|
|
339
|
+
// The framework's own bundle is reviewed with the framework: its bodies read the catalog and name tables by value.
|
|
340
|
+
if (context.framework) return
|
|
341
|
+
const framework = frameworkReference(tokens)
|
|
332
342
|
if (framework !== undefined) throw new Refused(`a reference to the framework's ${framework.name}`, framework.line)
|
|
333
343
|
const execute = tokens.find((token) => upperWord(token) === 'EXECUTE')
|
|
334
344
|
if (execute !== undefined) throw new Refused('a dynamic EXECUTE in a body', execute.line)
|
|
@@ -853,9 +863,9 @@ function classifyComment(tokens: readonly SqlToken[], context: Context): void {
|
|
|
853
863
|
}
|
|
854
864
|
|
|
855
865
|
/**
|
|
856
|
-
* The framework names a statement may use: a file collection's table (`fabrika_files_<name
|
|
857
|
-
*
|
|
858
|
-
*
|
|
866
|
+
* The framework names a statement may use: a file collection's table (`fabrika_files_<name>`) was created by the
|
|
867
|
+
* application's own migration under an older release's `fileTableSql`, with its status check and its indexes, and its
|
|
868
|
+
* `fileScanSql` replaced the status check. Every other framework name is refused.
|
|
859
869
|
*/
|
|
860
870
|
function fileTableNames(tokens: readonly SqlToken[]): ReadonlySet<string> {
|
|
861
871
|
const [first, second] = [upperWord(tokens[0]), upperWord(tokens[1])]
|
|
@@ -875,19 +885,54 @@ function fileTableNames(tokens: readonly SqlToken[]): ReadonlySet<string> {
|
|
|
875
885
|
}
|
|
876
886
|
|
|
877
887
|
/**
|
|
878
|
-
* The names of
|
|
879
|
-
*
|
|
888
|
+
* The names of an older release's `fileScanSql` statement, which replaces the status check of its collection table
|
|
889
|
+
* and adds the scan columns: that exact statement, and no other `ALTER` of a file table.
|
|
880
890
|
*/
|
|
881
891
|
function fileScanNames(tokens: readonly SqlToken[]): ReadonlySet<string> {
|
|
882
892
|
const table = fileScanAlterTable(tokens)
|
|
883
893
|
return table === undefined ? new Set() : new Set([table, legacyStatusCheckName(table), `${table}_status`])
|
|
884
894
|
}
|
|
885
895
|
|
|
896
|
+
/**
|
|
897
|
+
* The tokens a statement's framework names are checked over: without each `REFERENCES fabrika_files`, the foreign key
|
|
898
|
+
* of a file column (`id`, the table's key, is what it references).
|
|
899
|
+
*/
|
|
900
|
+
function withoutFileReferences(tokens: readonly SqlToken[]): readonly SqlToken[] {
|
|
901
|
+
return tokens.filter((token, index) => !(nameText(token) === STORED_FILES_TABLE && upperWord(tokens[index - 1]) === 'REFERENCES'))
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* The framework names an `INSERT` may read: a file collection's table, which an adoption migration copies the owner
|
|
906
|
+
* mapping out of (`INSERT INTO deal_attachments (…) SELECT … FROM fabrika_files_attachments`). It never writes one.
|
|
907
|
+
*/
|
|
908
|
+
function adoptedTableNames(tokens: readonly SqlToken[]): ReadonlySet<string> {
|
|
909
|
+
const target = nameText(tokens[skipWords(tokens, 1, 'INTO')])
|
|
910
|
+
if (upperWord(tokens[0]) !== 'INSERT' || target === undefined || isFrameworkName(target)) return new Set()
|
|
911
|
+
return new Set(tokens.flatMap((token) => {
|
|
912
|
+
const name = nameText(token)
|
|
913
|
+
return name?.startsWith(FILE_TABLE_PREFIX) === true ? [name] : []
|
|
914
|
+
}))
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* `SELECT fabrika_files_copy_legacy('<collection>');` and `SELECT fabrika_files_release('<collection>');`: the only
|
|
919
|
+
* calls of a framework function an application migration makes, each a statement of its own.
|
|
920
|
+
*/
|
|
921
|
+
function isFileAdoptionCall(tokens: readonly SqlToken[]): boolean {
|
|
922
|
+
const [select, name, open, collection, close] = tokens
|
|
923
|
+
const called = nameText(name)
|
|
924
|
+
return tokens.length === 5 && upperWord(select) === 'SELECT' && (called === FILES_COPY_FUNCTION || called === FILES_RELEASE_FUNCTION)
|
|
925
|
+
&& isPunct(open, '(')
|
|
926
|
+
&& collection?.kind === 'string' && /^[a-z_][a-z0-9_]*$/.test(collection.text ?? '') && isPunct(close, ')')
|
|
927
|
+
}
|
|
928
|
+
|
|
886
929
|
function classifyStatement(tokens: readonly SqlToken[], context: Context, out: Collected): void {
|
|
887
930
|
const line = tokens[0]?.line ?? 1
|
|
888
931
|
const unicode = unicodeIdentifier(tokens)
|
|
889
932
|
if (unicode !== undefined) throw new Refused('a Unicode-escaped identifier (U&"…")', unicode.line)
|
|
890
|
-
|
|
933
|
+
if (!context.framework && isFileAdoptionCall(tokens)) return
|
|
934
|
+
const allowed = new Set([...fileTableNames(tokens), ...adoptedTableNames(tokens)])
|
|
935
|
+
const framework = context.framework ? undefined : frameworkReference(withoutFileReferences(tokens), allowed)
|
|
891
936
|
if (framework !== undefined) throw new Refused(`a reference to the framework's ${framework.name}`, framework.line)
|
|
892
937
|
const first = upperWord(tokens[0])
|
|
893
938
|
if (first === 'INSERT' || first === 'UPDATE' || first === 'DELETE') return checkQuery(tokens, context, first)
|
package/src/sql.ts
CHANGED
|
@@ -33,6 +33,8 @@ export {
|
|
|
33
33
|
export { lexSql, type SqlToken, sqlTokens, type Statement, topLevelStatements, upperWord } from './migration-sql'
|
|
34
34
|
export { PG_CATALOG_FUNCTIONS, SQL_KEYWORDS } from './pg-catalog-names'
|
|
35
35
|
export {
|
|
36
|
+
afterName,
|
|
37
|
+
closingParenthesis,
|
|
36
38
|
ddlForeignSchema,
|
|
37
39
|
dmlForeignSchema,
|
|
38
40
|
dmlRelations,
|
|
@@ -40,13 +42,17 @@ export {
|
|
|
40
42
|
type FunctionUse,
|
|
41
43
|
functionUse,
|
|
42
44
|
isFrameworkName,
|
|
45
|
+
isPunct,
|
|
43
46
|
type NameReference,
|
|
44
47
|
nameReferences,
|
|
45
48
|
type NameRole,
|
|
46
49
|
nameText,
|
|
47
50
|
nestedStatement,
|
|
51
|
+
objectNameAt,
|
|
48
52
|
parenthesizedTarget,
|
|
49
53
|
refusedFunctionCall,
|
|
54
|
+
skipWords,
|
|
55
|
+
splitAtCommas,
|
|
50
56
|
statementKind,
|
|
51
57
|
unicodeIdentifier,
|
|
52
58
|
} from './sql-names'
|