@fabrika/platform-node 0.6.0 → 0.8.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabrika/platform-node",
3
- "version": "0.6.0",
3
+ "version": "0.8.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.6.0"
40
+ "@fabrika/platform": "0.8.0"
41
41
  }
42
42
  }
@@ -1,6 +1,6 @@
1
- // The SQL of a file collection's table that three readers must agree on: `@fabrika/db`'s `fileTableSql` and
2
- // `fileScanSql` write it, the migration classifier admits exactly the scan migration, and `@fabrika/check` recognizes
3
- // a table that can hold the scan states. Pure: tokens in, answers out.
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 earlier `fileTableSql` created:
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 a table an earlier `fileTableSql` created: its check and its columns. */
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 { FILE_TABLE_PREFIX, fileScanAlterTable, legacyStatusCheckName } from './file-table-ddl'
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
- const framework = context.framework ? undefined : frameworkReference(tokens)
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>`, `fileTableSql`) is
857
- * created by the application's own migration, with its status check and its indexes, and `fileScanSql` replaces an
858
- * earlier status check. Every other framework name is refused.
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 `@fabrika/db`'s `fileScanSql` statement, which replaces the status check of a file table an earlier
879
- * `fileTableSql` created and adds the scan columns: that exact statement, and no other `ALTER` of a file table.
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
- const framework = context.framework ? undefined : frameworkReference(tokens, fileTableNames(tokens))
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'