@owlmeans/postgres-resource 0.1.18-rc.42 → 0.1.18-rc.43

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.
Files changed (154) hide show
  1. package/README.md +13 -13
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/postgres-resource/SKILL.md +1 -1
  4. package/build/consts.d.ts +9 -0
  5. package/build/consts.d.ts.map +1 -1
  6. package/build/consts.js +6 -0
  7. package/build/consts.js.map +1 -1
  8. package/build/declarations/types.d.ts +12 -0
  9. package/build/declarations/types.d.ts.map +1 -0
  10. package/build/declarations/types.js +2 -0
  11. package/build/declarations/types.js.map +1 -0
  12. package/build/declarations.d.ts +5 -1
  13. package/build/declarations.d.ts.map +1 -1
  14. package/build/declarations.js +25 -15
  15. package/build/declarations.js.map +1 -1
  16. package/build/errors.d.ts +0 -20
  17. package/build/errors.d.ts.map +1 -1
  18. package/build/errors.js +1 -111
  19. package/build/errors.js.map +1 -1
  20. package/build/index.d.ts +3 -0
  21. package/build/index.d.ts.map +1 -1
  22. package/build/index.js +1 -0
  23. package/build/index.js.map +1 -1
  24. package/build/pg-error/types.d.ts +24 -0
  25. package/build/pg-error/types.d.ts.map +1 -0
  26. package/build/pg-error/types.js +2 -0
  27. package/build/pg-error/types.js.map +1 -0
  28. package/build/pg-error.d.ts +4 -0
  29. package/build/pg-error.d.ts.map +1 -0
  30. package/build/pg-error.js +100 -0
  31. package/build/pg-error.js.map +1 -0
  32. package/build/resource.d.ts.map +1 -1
  33. package/build/resource.js +15 -11
  34. package/build/resource.js.map +1 -1
  35. package/build/types.d.ts.map +1 -1
  36. package/build/utils/consts.local.d.ts +2 -0
  37. package/build/utils/consts.local.d.ts.map +1 -1
  38. package/build/utils/consts.local.js +2 -0
  39. package/build/utils/consts.local.js.map +1 -1
  40. package/build/utils/criteria/types.d.ts +31 -0
  41. package/build/utils/criteria/types.d.ts.map +1 -0
  42. package/build/utils/criteria/types.js +2 -0
  43. package/build/utils/criteria/types.js.map +1 -0
  44. package/build/utils/criteria.d.ts +3 -27
  45. package/build/utils/criteria.d.ts.map +1 -1
  46. package/build/utils/criteria.js +86 -104
  47. package/build/utils/criteria.js.map +1 -1
  48. package/build/utils/diff/types.d.ts +40 -0
  49. package/build/utils/diff/types.d.ts.map +1 -0
  50. package/build/utils/diff/types.js +2 -0
  51. package/build/utils/diff/types.js.map +1 -0
  52. package/build/utils/diff.d.ts +5 -35
  53. package/build/utils/diff.d.ts.map +1 -1
  54. package/build/utils/diff.js +320 -344
  55. package/build/utils/diff.js.map +1 -1
  56. package/build/utils/index.d.ts +9 -0
  57. package/build/utils/index.d.ts.map +1 -1
  58. package/build/utils/index.js.map +1 -1
  59. package/build/utils/introspect/types.d.ts +9 -0
  60. package/build/utils/introspect/types.d.ts.map +1 -0
  61. package/build/utils/introspect/types.js +2 -0
  62. package/build/utils/introspect/types.js.map +1 -0
  63. package/build/utils/introspect.d.ts +3 -3
  64. package/build/utils/introspect.d.ts.map +1 -1
  65. package/build/utils/introspect.js +41 -38
  66. package/build/utils/introspect.js.map +1 -1
  67. package/build/utils/life-cycle/types.d.ts +37 -0
  68. package/build/utils/life-cycle/types.d.ts.map +1 -0
  69. package/build/utils/life-cycle/types.js +2 -0
  70. package/build/utils/life-cycle/types.js.map +1 -0
  71. package/build/utils/life-cycle.d.ts +3 -33
  72. package/build/utils/life-cycle.d.ts.map +1 -1
  73. package/build/utils/life-cycle.js +88 -108
  74. package/build/utils/life-cycle.js.map +1 -1
  75. package/build/utils/marshal/types.d.ts +39 -0
  76. package/build/utils/marshal/types.d.ts.map +1 -0
  77. package/build/utils/marshal/types.js +2 -0
  78. package/build/utils/marshal/types.js.map +1 -0
  79. package/build/utils/marshal.d.ts +4 -32
  80. package/build/utils/marshal.d.ts.map +1 -1
  81. package/build/utils/marshal.js +121 -144
  82. package/build/utils/marshal.js.map +1 -1
  83. package/build/utils/migrations.d.ts.map +1 -1
  84. package/build/utils/migrations.js +41 -36
  85. package/build/utils/migrations.js.map +1 -1
  86. package/build/utils/name/types.d.ts +43 -0
  87. package/build/utils/name/types.d.ts.map +1 -0
  88. package/build/utils/name/types.js +2 -0
  89. package/build/utils/name/types.js.map +1 -0
  90. package/build/utils/name.d.ts +6 -35
  91. package/build/utils/name.d.ts.map +1 -1
  92. package/build/utils/name.js +38 -57
  93. package/build/utils/name.js.map +1 -1
  94. package/build/utils/schema/types.d.ts +22 -0
  95. package/build/utils/schema/types.d.ts.map +1 -0
  96. package/build/utils/schema/types.js +2 -0
  97. package/build/utils/schema/types.js.map +1 -0
  98. package/build/utils/schema.d.ts +4 -24
  99. package/build/utils/schema.d.ts.map +1 -1
  100. package/build/utils/schema.js +339 -352
  101. package/build/utils/schema.js.map +1 -1
  102. package/build/utils/sql/types.d.ts +25 -0
  103. package/build/utils/sql/types.d.ts.map +1 -0
  104. package/build/utils/sql/types.js +2 -0
  105. package/build/utils/sql/types.js.map +1 -0
  106. package/build/utils/sql.d.ts +4 -18
  107. package/build/utils/sql.d.ts.map +1 -1
  108. package/build/utils/sql.js +0 -0
  109. package/build/utils/sql.js.map +1 -1
  110. package/build/utils/sync/types.d.ts +25 -0
  111. package/build/utils/sync/types.d.ts.map +1 -0
  112. package/build/utils/sync/types.js +2 -0
  113. package/build/utils/sync/types.js.map +1 -0
  114. package/build/utils/sync.d.ts +2 -19
  115. package/build/utils/sync.d.ts.map +1 -1
  116. package/build/utils/sync.js +64 -71
  117. package/build/utils/sync.js.map +1 -1
  118. package/package.json +7 -7
  119. package/src/consts.ts +7 -0
  120. package/src/declarations/types.ts +12 -0
  121. package/src/declarations.ts +29 -14
  122. package/src/errors.ts +1 -121
  123. package/src/index.ts +3 -0
  124. package/src/pg-error/types.ts +23 -0
  125. package/src/pg-error.ts +114 -0
  126. package/src/resource.ts +17 -13
  127. package/src/types.ts +1 -0
  128. package/src/utils/consts.local.ts +3 -0
  129. package/src/utils/criteria/types.ts +32 -0
  130. package/src/utils/criteria.ts +95 -110
  131. package/src/utils/diff/types.ts +41 -0
  132. package/src/utils/diff.ts +330 -348
  133. package/src/utils/index.ts +9 -0
  134. package/src/utils/introspect/types.ts +9 -0
  135. package/src/utils/introspect.ts +51 -45
  136. package/src/utils/life-cycle/types.ts +41 -0
  137. package/src/utils/life-cycle.ts +102 -121
  138. package/src/utils/marshal/types.ts +40 -0
  139. package/src/utils/marshal.ts +129 -147
  140. package/src/utils/migrations.ts +39 -33
  141. package/src/utils/name/types.ts +44 -0
  142. package/src/utils/name.ts +47 -60
  143. package/src/utils/schema/types.ts +26 -0
  144. package/src/utils/schema.ts +359 -366
  145. package/src/utils/sql/types.ts +25 -0
  146. package/src/utils/sql.ts +0 -0
  147. package/src/utils/sync/types.ts +25 -0
  148. package/src/utils/sync.ts +62 -68
  149. package/tests/criteria.spec.ts +4 -1
  150. package/tests/diff.spec.ts +4 -1
  151. package/tests/errors.spec.ts +4 -3
  152. package/tests/name.spec.ts +5 -3
  153. package/tests/schema.spec.ts +3 -1
  154. package/tests/sql.spec.ts +20 -22
@@ -0,0 +1,25 @@
1
+ import type { TableSpec } from '../../types.js'
2
+
3
+ /**
4
+ * `{{alias}}` placeholders of custom SQL, resolved through ONE context's registered postgres
5
+ * resources. Reached as `pgPlaceholdersOf(context)`.
6
+ */
7
+ export interface PgPlaceholderHelper {
8
+ /**
9
+ * Fully qualified table of a registered postgres resource; omit the alias for `self`.
10
+ * This is what `PostgresTx.ref` and `PostgresResource.ref` resolve through.
11
+ *
12
+ * @throws {PostgresPlaceholderError}
13
+ */
14
+ refOf: (self: TableSpec | null, alias?: string) => string
15
+ /**
16
+ * Rewrite `{{...}}` placeholders into quoted identifiers.
17
+ *
18
+ * Identifiers only — `$1`-style parameters are left exactly as written and their values
19
+ * never pass through here. Postgres can't bind an identifier as a parameter, which is the
20
+ * whole reason this mechanism has to exist; values have no such excuse.
21
+ *
22
+ * @throws {PostgresPlaceholderError}
23
+ */
24
+ resolvePlaceholders: (text: string, self: TableSpec | null) => string
25
+ }
package/src/utils/sql.ts CHANGED
Binary file
@@ -0,0 +1,25 @@
1
+ import type { DdlPlan, TableSpec } from '../../types.js'
2
+
3
+ /** The reconciliation steps run over one checked out connection. */
4
+ export interface PgSyncHelper {
5
+ /**
6
+ * Serialize initialization across replicas. A session level lock, not `xact`, because
7
+ * migrations run in their own transactions inside the critical section.
8
+ */
9
+ acquireLock: (qualified: string) => Promise<void>
10
+ /** Release the {@link PgSyncHelper.acquireLock} lock; never masks the failure that preceded it. */
11
+ releaseLock: (qualified: string) => Promise<void>
12
+ /** `CREATE SCHEMA IF NOT EXISTS`. */
13
+ ensureSchema: (schema: string) => Promise<void>
14
+ /**
15
+ * Apply a reconciliation plan in a single transaction.
16
+ *
17
+ * Postgres DDL is transactional, so a plan that fails partway leaves the table exactly as
18
+ * it was. That property is what makes converging without confirmation prompts tolerable —
19
+ * the table is either fully converged or untouched, never half migrated.
20
+ *
21
+ * @throws {PostgresCastRequired} Postgres refused an automatic cast.
22
+ * @throws {PostgresSyncError} any other statement failed — the message names the statement.
23
+ */
24
+ applyPlan: (spec: TableSpec, plan: DdlPlan) => Promise<DdlPlan>
25
+ }
package/src/utils/sync.ts CHANGED
@@ -1,94 +1,88 @@
1
1
  import { logger } from '@owlmeans/log'
2
2
  import type { PoolClient } from 'pg'
3
3
 
4
- import { PostgresCastRequired, PostgresSyncError, describePgError, pgErrorToResourceError } from '../errors.js'
4
+ import { PostgresCastRequired, PostgresSyncError } from '../errors.js'
5
+ import { pgErrorHelper } from '../pg-error.js'
5
6
  import type { DdlPlan, TableSpec } from '../types.js'
6
- import { countNonNull } from './introspect.js'
7
- import { advisoryKey, quoteIdent } from './name.js'
7
+ import { makePgIntrospectHelper } from './introspect.js'
8
+ import { pgNameHelper } from './name.js'
8
9
  import { CAST_DATA_CODES } from './consts.local.js'
10
+ import type { PgSyncHelper } from './sync/types.js'
9
11
 
10
12
  const log = logger('postgres-resource')
11
13
 
12
- /**
13
- * Serialize initialization across replicas. A session level lock, not `xact`, because
14
- * migrations run in their own transactions inside the critical section.
15
- */
16
- export const acquireLock = async (client: PoolClient, qualified: string): Promise<void> => {
17
- const [first, second] = advisoryKey(qualified)
18
- await client.query('SELECT pg_advisory_lock($1, $2)', [first, second])
19
- }
14
+ export const makePgSyncHelper = (client: PoolClient): PgSyncHelper => {
15
+ const { describePgError, pgErrorToResourceError } = pgErrorHelper
16
+ const { advisoryKey, quoteIdent } = pgNameHelper
17
+ const introspect = makePgIntrospectHelper(client)
20
18
 
21
- export const releaseLock = async (client: PoolClient, qualified: string): Promise<void> => {
22
- const [first, second] = advisoryKey(qualified)
23
- try {
24
- await client.query('SELECT pg_advisory_unlock($1, $2)', [first, second])
25
- } catch {
26
- /** Releasing the session drops the lock anyway — never mask the original failure. */
19
+ const acquireLock = async (qualified: string): Promise<void> => {
20
+ const [first, second] = advisoryKey(qualified)
21
+ await client.query('SELECT pg_advisory_lock($1, $2)', [first, second])
27
22
  }
28
- }
29
23
 
30
- export const ensureSchema = async (client: PoolClient, schema: string): Promise<void> => {
31
- try {
32
- await client.query(`CREATE SCHEMA IF NOT EXISTS ${quoteIdent(schema)}`)
33
- } catch (error) {
34
- throw pgErrorToResourceError(error)
24
+ const releaseLock = async (qualified: string): Promise<void> => {
25
+ const [first, second] = advisoryKey(qualified)
26
+ try {
27
+ await client.query('SELECT pg_advisory_unlock($1, $2)', [first, second])
28
+ } catch {
29
+ /** Releasing the session drops the lock anyway — never mask the original failure. */
30
+ }
35
31
  }
36
- }
37
32
 
38
- /**
39
- * Apply a reconciliation plan in a single transaction.
40
- *
41
- * Postgres DDL is transactional, so a plan that fails partway leaves the table exactly as
42
- * it was. That property is what makes converging without confirmation prompts tolerable —
43
- * the table is either fully converged or untouched, never half migrated.
44
- *
45
- * @throws {PostgresCastRequired} Postgres refused an automatic cast.
46
- * @throws {PostgresSyncError} any other statement failed — the message names the statement.
47
- */
48
- export const applyPlan = async (
49
- client: PoolClient, spec: TableSpec, plan: DdlPlan
50
- ): Promise<DdlPlan> => {
51
- if (plan.statements.length < 1) {
52
- return plan
33
+ const ensureSchema = async (schema: string): Promise<void> => {
34
+ try {
35
+ await client.query(`CREATE SCHEMA IF NOT EXISTS ${quoteIdent(schema)}`)
36
+ } catch (error) {
37
+ throw pgErrorToResourceError(error)
38
+ }
53
39
  }
54
40
 
55
- /** Read what a drop would cost before the transaction opens, so the log can say it. */
56
- for (const statement of plan.statements) {
57
- if (statement.destructive === true) {
58
- statement.affected = await countNonNull(client, spec.qualified, statement.target)
41
+ const applyPlan = async (spec: TableSpec, plan: DdlPlan): Promise<DdlPlan> => {
42
+ if (plan.statements.length < 1) {
43
+ return plan
59
44
  }
60
- }
61
45
 
62
- await client.query('BEGIN')
63
- let current = ''
64
- try {
46
+ /** Read what a drop would cost before the transaction opens, so the log can say it. */
65
47
  for (const statement of plan.statements) {
66
- current = statement.sql
67
- if (statement.destructive === true && (statement.affected ?? 0) > 0) {
68
- log.warn('Dropping a column that holds values; declare it under pg.unmanaged or set pg.managed false to keep it', {
69
- table: spec.qualified, column: statement.target, rows: statement.affected,
70
- }, { event: 'migration.drop' })
48
+ if (statement.destructive === true) {
49
+ statement.affected = await introspect.countNonNull(spec.qualified, statement.target)
71
50
  }
72
- await client.query(statement.sql)
73
51
  }
74
- await client.query('COMMIT')
75
- } catch (error) {
76
- await client.query('ROLLBACK').catch(() => undefined)
77
- const translated = pgErrorToResourceError(error)
78
- const code = (error as { code?: string } | null)?.code
79
- if (translated instanceof PostgresCastRequired || (code != null && CAST_DATA_CODES.includes(code))) {
80
- const failure = new PostgresCastRequired(
81
- `${spec.qualified}: ${describePgError(error)} — statement: ${current}.`
82
- + ' Declare `pg: { using: \'<expr>\' }` on the property, or perform the change in a'
83
- + ' `pre` migration so reconciliation observes no drift.'
84
- )
52
+
53
+ await client.query('BEGIN')
54
+ let current = ''
55
+ try {
56
+ for (const statement of plan.statements) {
57
+ current = statement.sql
58
+ if (statement.destructive === true && (statement.affected ?? 0) > 0) {
59
+ log.warn('Dropping a column that holds values; declare it under pg.unmanaged or set pg.managed false to keep it', {
60
+ table: spec.qualified, column: statement.target, rows: statement.affected,
61
+ }, { event: 'migration.drop' })
62
+ }
63
+ await client.query(statement.sql)
64
+ }
65
+ await client.query('COMMIT')
66
+ } catch (error) {
67
+ await client.query('ROLLBACK').catch(() => undefined)
68
+ const translated = pgErrorToResourceError(error)
69
+ const code = (error as { code?: string } | null)?.code
70
+ if (translated instanceof PostgresCastRequired || (code != null && CAST_DATA_CODES.includes(code))) {
71
+ const failure = new PostgresCastRequired(
72
+ `${spec.qualified}: ${describePgError(error)} — statement: ${current}.`
73
+ + ' Declare `pg: { using: \'<expr>\' }` on the property, or perform the change in a'
74
+ + ' `pre` migration so reconciliation observes no drift.'
75
+ )
76
+ failure.cause = error
77
+ throw failure
78
+ }
79
+ const failure = new PostgresSyncError(`${spec.qualified}: ${describePgError(error)} — statement: ${current}`)
85
80
  failure.cause = error
86
81
  throw failure
87
82
  }
88
- const failure = new PostgresSyncError(`${spec.qualified}: ${describePgError(error)} — statement: ${current}`)
89
- failure.cause = error
90
- throw failure
83
+
84
+ return plan
91
85
  }
92
86
 
93
- return plan
87
+ return { acquireLock, releaseLock, ensureSchema, applyPlan }
94
88
  }
@@ -6,9 +6,12 @@ import { sql } from 'drizzle-orm'
6
6
  import type { SQL } from 'drizzle-orm'
7
7
  import { PgDialect } from 'drizzle-orm/pg-core'
8
8
 
9
- import { criteriaToSql, schemaToTableSpec, sortToSql, specToTable } from '@owlmeans/postgres-resource'
9
+ import { pgCriteriaHelper, pgSchemaHelper, specToTable } from '@owlmeans/postgres-resource'
10
10
  import type { PgRuntimeTable, TableSpec } from '@owlmeans/postgres-resource'
11
11
 
12
+ const { criteriaToSql, sortToSql } = pgCriteriaHelper
13
+ const { schemaToTableSpec } = pgSchemaHelper
14
+
12
15
  /**
13
16
  * Criteria translation is where a typo becomes either an error or a whole-table scan, and
14
17
  * where a value has to stay a bound parameter instead of becoming statement text. Neither
@@ -1,8 +1,11 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
 
3
- import { canonicalDefinition, planSync, schemaToTableSpec } from '@owlmeans/postgres-resource'
3
+ import { pgDiffHelper, pgSchemaHelper } from '@owlmeans/postgres-resource'
4
4
  import type { LiveColumn, LiveTable, TableSpec } from '@owlmeans/postgres-resource'
5
5
 
6
+ const { canonicalDefinition, planSync } = pgDiffHelper
7
+ const { schemaToTableSpec } = pgSchemaHelper
8
+
6
9
  const specOf = (idType: 'string' | 'integer'): TableSpec =>
7
10
  schemaToTableSpec('tasks', {
8
11
  type: 'object',
@@ -2,11 +2,12 @@ import { describe, expect, test } from 'bun:test'
2
2
  import { MisshapedRecord, RecordExists, ResourceError } from '@owlmeans/resource'
3
3
 
4
4
  import {
5
- describePgError, PgErrorCode, PostgresCastRequired, PostgresCheckError, PostgresConnectionError,
6
- PostgresConstraintError, PostgresDeadlockError, PostgresError, PostgresForeignKeyError,
7
- pgErrorToResourceError
5
+ PgErrorCode, pgErrorHelper, PostgresCastRequired, PostgresCheckError, PostgresConnectionError,
6
+ PostgresConstraintError, PostgresDeadlockError, PostgresError, PostgresForeignKeyError
8
7
  } from '@owlmeans/postgres-resource'
9
8
 
9
+ const { describePgError, pgErrorToResourceError } = pgErrorHelper
10
+
10
11
  interface DriverShape {
11
12
  code: string
12
13
  message?: string
@@ -1,11 +1,13 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
  import type { DbConfig, ResourceRecord } from '@owlmeans/resource'
3
3
 
4
- import {
5
- advisoryKey, assertSqlIdentifier, pgIdentifier, pgTableName, qualify, quoteIdent, quoteLiteral
6
- } from '@owlmeans/postgres-resource'
4
+ import { pgNameHelper } from '@owlmeans/postgres-resource'
7
5
  import type { PostgresResource } from '@owlmeans/postgres-resource'
8
6
 
7
+ const {
8
+ advisoryKey, assertSqlIdentifier, pgIdentifier, pgTableName, qualify, quoteIdent, quoteLiteral
9
+ } = pgNameHelper
10
+
9
11
  const resource = (alias: string, name?: string): PostgresResource<ResourceRecord> =>
10
12
  ({ alias, ...(name != null ? { name } : {}) }) as PostgresResource<ResourceRecord>
11
13
 
@@ -2,9 +2,11 @@ import { describe, expect, test } from 'bun:test'
2
2
  import { UnsupportedArgumentError } from '@owlmeans/resource'
3
3
  import type { AnySchema } from 'ajv'
4
4
 
5
- import { schemaToTableSpec, toFormatType } from '@owlmeans/postgres-resource'
5
+ import { pgSchemaHelper } from '@owlmeans/postgres-resource'
6
6
  import type { TableSpec } from '@owlmeans/postgres-resource'
7
7
 
8
+ const { schemaToTableSpec, toFormatType } = pgSchemaHelper
9
+
8
10
  /**
9
11
  * Compiling an AJV schema into a table specification is the one step every other part of
10
12
  * the package trusts: DDL emission, drift detection, marshalling and placeholder
package/tests/sql.spec.ts CHANGED
@@ -1,13 +1,11 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
  import type { BasicContext } from '@owlmeans/context'
3
3
 
4
- import {
5
- PostgresPlaceholderError, refOf, resolvePlaceholders, schemaToTableSpec
6
- } from '@owlmeans/postgres-resource'
4
+ import { pgPlaceholdersOf, pgSchemaHelper, PostgresPlaceholderError } from '@owlmeans/postgres-resource'
7
5
  import type { TableSpec } from '@owlmeans/postgres-resource'
8
6
 
9
7
  const specOf = (alias: string, schema: string, table: string): TableSpec =>
10
- schemaToTableSpec(alias, {
8
+ pgSchemaHelper.schemaToTableSpec(alias, {
11
9
  type: 'object',
12
10
  properties: {
13
11
  id: { type: 'string', format: 'uuid' },
@@ -37,47 +35,47 @@ const context = contextOf({ users, posts })
37
35
 
38
36
  describe('@owlmeans/postgres-resource — placeholder resolution', () => {
39
37
  test('resolves the owning resource', () => {
40
- expect(resolvePlaceholders('SELECT * FROM {{}}', context, posts))
38
+ expect(pgPlaceholdersOf(context).resolvePlaceholders('SELECT * FROM {{}}', posts))
41
39
  .toBe('SELECT * FROM "app"."posts"')
42
- expect(resolvePlaceholders('SELECT * FROM {{ self }}', context, posts))
40
+ expect(pgPlaceholdersOf(context).resolvePlaceholders('SELECT * FROM {{ self }}', posts))
43
41
  .toBe('SELECT * FROM "app"."posts"')
44
42
  })
45
43
 
46
44
  test('resolves another registered resource, its columns, its bare name and its schema', () => {
47
- expect(resolvePlaceholders('SELECT * FROM {{users}}', context, posts))
45
+ expect(pgPlaceholdersOf(context).resolvePlaceholders('SELECT * FROM {{users}}', posts))
48
46
  .toBe('SELECT * FROM "app"."users"')
49
47
  /** The physical column, not the property — a `pg: { column }` rename has to survive. */
50
- expect(resolvePlaceholders('SELECT {{users.email}}', context, posts))
48
+ expect(pgPlaceholdersOf(context).resolvePlaceholders('SELECT {{users.email}}', posts))
51
49
  .toBe('SELECT "app"."users"."email_address"')
52
- expect(resolvePlaceholders('ON CONSTRAINT {{#users}}', context, posts))
50
+ expect(pgPlaceholdersOf(context).resolvePlaceholders('ON CONSTRAINT {{#users}}', posts))
53
51
  .toBe('ON CONSTRAINT "users"')
54
- expect(resolvePlaceholders('SET search_path = {{$}}', context, posts))
52
+ expect(pgPlaceholdersOf(context).resolvePlaceholders('SET search_path = {{$}}', posts))
55
53
  .toBe('SET search_path = "app"')
56
54
  })
57
55
 
58
56
  test('leaves bound parameters exactly as written', () => {
59
57
  const text = `SELECT * FROM {{}} WHERE "id" = $1 AND "email" = $2 AND note = '{{not-a-placeholder'`
60
- expect(resolvePlaceholders(text, context, posts))
58
+ expect(pgPlaceholdersOf(context).resolvePlaceholders(text, posts))
61
59
  .toBe(`SELECT * FROM "app"."posts" WHERE "id" = $1 AND "email" = $2 AND note = '{{not-a-placeholder'`)
62
60
  })
63
61
 
64
62
  test('refuses an alias it cannot resolve rather than substituting blindly', () => {
65
- expect(() => resolvePlaceholders('SELECT * FROM {{nope}}', context, posts))
63
+ expect(() => pgPlaceholdersOf(context).resolvePlaceholders('SELECT * FROM {{nope}}', posts))
66
64
  .toThrow(PostgresPlaceholderError)
67
- expect(() => resolvePlaceholders('SELECT {{users.nope}}', context, posts))
65
+ expect(() => pgPlaceholdersOf(context).resolvePlaceholders('SELECT {{users.nope}}', posts))
68
66
  .toThrow(PostgresPlaceholderError)
69
67
  /** Registered but not yet initialized: the table name genuinely isn't known. */
70
- expect(() => resolvePlaceholders('SELECT * FROM {{late}}', contextOf({ late: null }), posts))
68
+ expect(() => pgPlaceholdersOf(contextOf({ late: null })).resolvePlaceholders('SELECT * FROM {{late}}', posts))
71
69
  .toThrow(PostgresPlaceholderError)
72
70
  })
73
71
 
74
72
  test('refuses a self reference in a query that has no owning resource', () => {
75
- expect(() => resolvePlaceholders('SELECT * FROM {{}}', context, null))
73
+ expect(() => pgPlaceholdersOf(context).resolvePlaceholders('SELECT * FROM {{}}', null))
76
74
  .toThrow(PostgresPlaceholderError)
77
- expect(() => resolvePlaceholders('SET search_path = {{$}}', context, null))
75
+ expect(() => pgPlaceholdersOf(context).resolvePlaceholders('SET search_path = {{$}}', null))
78
76
  .toThrow(PostgresPlaceholderError)
79
77
  /** Other aliases stay addressable — a service level query is scopeless, not blind. */
80
- expect(resolvePlaceholders('SELECT * FROM {{users}}', context, null))
78
+ expect(pgPlaceholdersOf(context).resolvePlaceholders('SELECT * FROM {{users}}', null))
81
79
  .toBe('SELECT * FROM "app"."users"')
82
80
  })
83
81
 
@@ -91,15 +89,15 @@ describe('@owlmeans/postgres-resource — placeholder resolution', () => {
91
89
  const tenantA = contextOf({ users: specOf('users', 'tenant_a', 'users') })
92
90
  const tenantB = contextOf({ users: specOf('users', 'tenant_b', 'users') })
93
91
 
94
- expect(resolvePlaceholders('SELECT * FROM {{users}}', tenantA, null))
92
+ expect(pgPlaceholdersOf(tenantA).resolvePlaceholders('SELECT * FROM {{users}}', null))
95
93
  .toBe('SELECT * FROM "tenant_a"."users"')
96
- expect(resolvePlaceholders('SELECT * FROM {{users}}', tenantB, null))
94
+ expect(pgPlaceholdersOf(tenantB).resolvePlaceholders('SELECT * FROM {{users}}', null))
97
95
  .toBe('SELECT * FROM "tenant_b"."users"')
98
96
  })
99
97
 
100
98
  test('refOf answers the same question without any SQL around it', () => {
101
- expect(refOf(context, posts)).toBe('"app"."posts"')
102
- expect(refOf(context, posts, 'users')).toBe('"app"."users"')
103
- expect(() => refOf(context, null)).toThrow(PostgresPlaceholderError)
99
+ expect(pgPlaceholdersOf(context).refOf(posts)).toBe('"app"."posts"')
100
+ expect(pgPlaceholdersOf(context).refOf(posts, 'users')).toBe('"app"."users"')
101
+ expect(() => pgPlaceholdersOf(context).refOf(null)).toThrow(PostgresPlaceholderError)
104
102
  })
105
103
  })