pipework 0.279.0 → 0.281.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/REFERENCE.md +1 -0
  3. package/dist/async/jobs/claims.d.ts +15 -1
  4. package/dist/async/jobs/claims.d.ts.map +1 -1
  5. package/dist/async/jobs/claims.js.map +1 -1
  6. package/dist/async/jobs/index.d.ts +1 -1
  7. package/dist/async/jobs/index.d.ts.map +1 -1
  8. package/dist/async/jobs/queue.d.ts +28 -8
  9. package/dist/async/jobs/queue.d.ts.map +1 -1
  10. package/dist/async/jobs/queue.js +71 -9
  11. package/dist/async/jobs/queue.js.map +1 -1
  12. package/dist/async/jobs/serialize-error.d.ts +2 -0
  13. package/dist/async/jobs/serialize-error.d.ts.map +1 -1
  14. package/dist/async/jobs/serialize-error.js +5 -4
  15. package/dist/async/jobs/serialize-error.js.map +1 -1
  16. package/dist/cli/commands/migrate.d.ts +4 -0
  17. package/dist/cli/commands/migrate.d.ts.map +1 -1
  18. package/dist/cli/commands/migrate.js +17 -2
  19. package/dist/cli/commands/migrate.js.map +1 -1
  20. package/dist/data/migrate/apply.d.ts +13 -0
  21. package/dist/data/migrate/apply.d.ts.map +1 -1
  22. package/dist/data/migrate/apply.js +28 -3
  23. package/dist/data/migrate/apply.js.map +1 -1
  24. package/dist/data/migrate/run.d.ts +12 -1
  25. package/dist/data/migrate/run.d.ts.map +1 -1
  26. package/dist/data/migrate/run.js +8 -1
  27. package/dist/data/migrate/run.js.map +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/package.json +1 -1
  31. package/src/async/jobs/claims.ts +11 -1
  32. package/src/async/jobs/index.ts +1 -1
  33. package/src/async/jobs/queue.ts +100 -15
  34. package/src/async/jobs/serialize-error.ts +5 -4
  35. package/src/cli/commands/migrate.ts +18 -3
  36. package/src/data/migrate/apply.ts +38 -3
  37. package/src/data/migrate/run.ts +22 -3
  38. package/src/index.ts +1 -1
@@ -18,7 +18,8 @@ const DROPPED_KEYS = new Set(['params', 'parameters', 'args', 'types'])
18
18
  const MAX_QUERY_CHARS = 2_000
19
19
  const MAX_STRING_CHARS = 4_000
20
20
 
21
- function cut(text: string, max: number): string {
21
+ /** Cuts `text` to `max` characters, ending `…[truncated N chars]` when it was longer. */
22
+ export function cutText(text: string, max: number): string {
22
23
  return text.length <= max ? text : `${text.slice(0, max)}…[truncated ${String(text.length - max)} chars]`
23
24
  }
24
25
 
@@ -33,13 +34,13 @@ function serialize(value: unknown, seen: Set<Error>): unknown {
33
34
  const stack = typeof value.stack === 'string' && paramsAt !== -1 ? value.stack.replace(value.message, message) : value.stack
34
35
  const out: Record<string, unknown> = {
35
36
  name: value.name,
36
- message: cut(message, MAX_STRING_CHARS),
37
- stack: typeof stack === 'string' ? cut(stack, MAX_STRING_CHARS) : stack,
37
+ message: cutText(message, MAX_STRING_CHARS),
38
+ stack: typeof stack === 'string' ? cutText(stack, MAX_STRING_CHARS) : stack,
38
39
  }
39
40
  for (const key of Object.keys(value)) {
40
41
  if (key === 'name' || key === 'message' || key === 'stack' || key === 'cause' || DROPPED_KEYS.has(key)) continue
41
42
  const v = (value as unknown as Record<string, unknown>)[key]
42
- out[key] = typeof v === 'string' ? cut(v, key === 'query' ? MAX_QUERY_CHARS : MAX_STRING_CHARS) : v
43
+ out[key] = typeof v === 'string' ? cutText(v, key === 'query' ? MAX_QUERY_CHARS : MAX_STRING_CHARS) : v
43
44
  }
44
45
  if (value.cause !== undefined) {
45
46
  out['cause'] = serialize(value.cause, seen)
@@ -1,5 +1,5 @@
1
- import { info, success, error as logError, warn } from '../output.js'
2
- import type { MigrateResult } from '../../data/migrate/run.js'
1
+ import { info, success, error as logError, warn, yellow, cyan } from '../output.js'
2
+ import type { MigrateResult, MigrateNotice } from '../../data/migrate/run.js'
3
3
  import { frameworkTablesChanged } from '../../data/migrate/provision-framework.js'
4
4
 
5
5
  export interface MigrateCommandOptions {
@@ -44,7 +44,7 @@ export async function migrate(options: MigrateCommandOptions): Promise<void> {
44
44
  warn('Running migrations against PRODUCTION database.')
45
45
  }
46
46
 
47
- const migrateOptions = {}
47
+ const migrateOptions = { onNotice: printNotice }
48
48
 
49
49
  try {
50
50
  if (options.db !== undefined) {
@@ -130,3 +130,18 @@ function reportFrameworkTables(result: MigrateResult): void {
130
130
  `${f.partitionsCreated.length} day partitions created`,
131
131
  )
132
132
  }
133
+
134
+ /** One notice or warning a migration statement raised, indented under its database. Issue #970. */
135
+ export function formatNotice(n: MigrateNotice): string {
136
+ const label = n.severity === 'NOTICE' || n.severity === 'INFO' || n.severity === 'LOG' || n.severity === 'DEBUG'
137
+ ? cyan(n.severity)
138
+ : yellow(n.severity)
139
+ let out = ` ${label} ${n.tag} (statement ${n.statement}/${n.statements}): ${n.message}\n`
140
+ if (n.detail !== undefined) out += ` DETAIL: ${n.detail}\n`
141
+ if (n.hint !== undefined) out += ` HINT: ${n.hint}\n`
142
+ return out
143
+ }
144
+
145
+ export function printNotice(n: MigrateNotice): void {
146
+ process.stderr.write(formatNotice(n))
147
+ }
@@ -7,6 +7,14 @@ import { listMigrations } from './history.js'
7
7
 
8
8
  const NO_TRANSACTION_PRAGMA = '-- pipework:no-transaction'
9
9
 
10
+ /** A NOTICE or WARNING (or other non-error message) a migration statement raised. */
11
+ export interface MigrationNotice {
12
+ severity: string
13
+ message: string
14
+ detail?: string
15
+ hint?: string
16
+ }
17
+
10
18
  export interface ApplyOptions {
11
19
  migrationsFolder: string
12
20
  migrationsTable: string
@@ -44,6 +52,12 @@ export interface ApplyOptions {
44
52
  * Issue #378.
45
53
  */
46
54
  onResume?: (tag: string, fromStatement: number, total: number) => void
55
+ /**
56
+ * Called for each notice or warning a migration file's own statement raises,
57
+ * with the file's tag, the 1-based statement and the file's statement count.
58
+ * The runner's own bookkeeping statements never reach it. Issue #970.
59
+ */
60
+ onNotice?: (tag: string, statement: number, statements: number, notice: MigrationNotice) => void
47
61
  }
48
62
 
49
63
  export interface Migration {
@@ -60,7 +74,28 @@ export async function applyMigrations(options: ApplyOptions): Promise<number> {
60
74
  const migrations = readMigrations(options)
61
75
  if (migrations.length === 0) return 0
62
76
 
63
- const client = postgres(options.connectionUrl, { max: 1, onnotice: () => {} })
77
+ // Set only while a statement from a migration file runs; the runner's own
78
+ // bookkeeping raises "already exists, skipping" notices that are not the
79
+ // migration's and are dropped.
80
+ let current: { tag: string; statement: number; statements: number } | undefined
81
+ const client = postgres(options.connectionUrl, {
82
+ max: 1,
83
+ onnotice: (n) => {
84
+ if (current === undefined || options.onNotice === undefined) return
85
+ const notice: MigrationNotice = { severity: String(n['severity']), message: String(n['message']) }
86
+ if (typeof n['detail'] === 'string') notice.detail = n['detail']
87
+ if (typeof n['hint'] === 'string') notice.hint = n['hint']
88
+ options.onNotice(current.tag, current.statement, current.statements, notice)
89
+ },
90
+ })
91
+ const inStatement = async <T>(tag: string, statement: number, statements: number, run: () => Promise<T>): Promise<T> => {
92
+ current = { tag, statement, statements }
93
+ try {
94
+ return await run()
95
+ } finally {
96
+ current = undefined
97
+ }
98
+ }
64
99
 
65
100
  try {
66
101
  const table = quoteIdent(options.migrationsTable)
@@ -158,7 +193,7 @@ export async function applyMigrations(options: ApplyOptions): Promise<number> {
158
193
  if (from > 0) options.onResume?.(migration.tag, from + 1, migration.statements.length)
159
194
 
160
195
  for (let i = from; i < migration.statements.length; i++) {
161
- await client.unsafe(migration.statements[i]!)
196
+ await inStatement(migration.tag, i + 1, migration.statements.length, () => client.unsafe(migration.statements[i]!))
162
197
  // Recorded AFTER the statement it describes: marking first would let
163
198
  // a death between the mark and the statement skip work silently,
164
199
  // which is worse than replaying one statement. The window that
@@ -179,7 +214,7 @@ export async function applyMigrations(options: ApplyOptions): Promise<number> {
179
214
  } else {
180
215
  await client.begin(async (tx) => {
181
216
  for (const [i, stmt] of migration.statements.entries()) {
182
- await tx.unsafe(stmt)
217
+ await inStatement(migration.tag, i + 1, migration.statements.length, () => tx.unsafe(stmt))
183
218
  options.onStatement?.(migration.tag, i + 1, migration.statements.length, applied + 1, migrations.length)
184
219
  }
185
220
  await tx.unsafe(
@@ -5,14 +5,17 @@ import { quoteIdentifier } from '../db/identifiers.js'
5
5
  import { OWNER_LABEL_MIGRATE } from '../db/owner-labels.js'
6
6
  import { checkMigrations, type MigrationCheckResult } from './check.js'
7
7
  import { readAppliedState } from './applied.js'
8
- import { applyMigrations } from './apply.js'
8
+ import { applyMigrations, type MigrationNotice } from './apply.js'
9
9
  import { provisionFrameworkTables, type FrameworkTablesResult } from './provision-framework.js'
10
10
 
11
11
  // Deliberately empty of hazard switches. Authorization for a hazardous
12
12
  // migration is declared in that migration's own file (see pragma.ts) — an
13
13
  // option here would be an invocation-level approval, which is exactly the
14
14
  // thing that gets written into a deploy workflow once and never revisited.
15
- export interface MigrateOptions {}
15
+ export interface MigrateOptions {
16
+ /** Called as each notice or warning a migration statement raises arrives. Issue #970. */
17
+ onNotice?: (notice: MigrateNotice) => void
18
+ }
16
19
 
17
20
  export interface MigrateResult {
18
21
  database: string
@@ -21,6 +24,8 @@ export interface MigrateResult {
21
24
  safetyCheck: MigrationCheckResult
22
25
  /** No-transaction files picked up half applied and finished. Issue #378. */
23
26
  resumed: ResumedMigration[]
27
+ /** Notices and warnings raised by migration statements, in arrival order. Issue #970. */
28
+ notices: MigrateNotice[]
24
29
  /** Trace/audit tables, columns and day partitions this run provisioned. */
25
30
  frameworkTables: FrameworkTablesResult
26
31
  }
@@ -32,6 +37,13 @@ export interface ResumedMigration {
32
37
  totalStatements: number
33
38
  }
34
39
 
40
+ export interface MigrateNotice extends MigrationNotice {
41
+ tag: string
42
+ /** 1-based statement within the file. */
43
+ statement: number
44
+ statements: number
45
+ }
46
+
35
47
  export class UnsafeMigrationError extends Error {
36
48
  override readonly name = 'UnsafeMigrationError'
37
49
  constructor(
@@ -54,7 +66,7 @@ export async function migrateAll(instance: Manifold, options: MigrateOptions = {
54
66
  return results
55
67
  }
56
68
 
57
- export async function migrateOne(instance: Manifold, dbConfig: ResolvedDatabase, _options: MigrateOptions = {}): Promise<MigrateResult> {
69
+ export async function migrateOne(instance: Manifold, dbConfig: ResolvedDatabase, options: MigrateOptions = {}): Promise<MigrateResult> {
58
70
  const conn = instance.pool.getOrCreate(dbConfig, 'owner', OWNER_LABEL_MIGRATE)
59
71
 
60
72
  // Scope the safety gate to pending migrations only — hazards in already-applied
@@ -87,11 +99,17 @@ export async function migrateOne(instance: Manifold, dbConfig: ResolvedDatabase,
87
99
  }
88
100
 
89
101
  const resumed: ResumedMigration[] = []
102
+ const notices: MigrateNotice[] = []
90
103
  const migrationsRun = await applyMigrations({
91
104
  migrationsFolder: dbConfig.migrations!,
92
105
  migrationsTable: dbConfig.migrationTable,
93
106
  connectionUrl: dbConfig.url,
94
107
  onResume: (tag, fromStatement, totalStatements) => resumed.push({ tag, fromStatement, totalStatements }),
108
+ onNotice: (tag, statement, statements, notice) => {
109
+ const full: MigrateNotice = { tag, statement, statements, ...notice }
110
+ notices.push(full)
111
+ options.onNotice?.(full)
112
+ },
95
113
  })
96
114
 
97
115
  // Trace and audit storage rides the default database; their partitioned
@@ -115,6 +133,7 @@ export async function migrateOne(instance: Manifold, dbConfig: ResolvedDatabase,
115
133
  extensions: extensionsCreated,
116
134
  safetyCheck,
117
135
  resumed,
136
+ notices,
118
137
  frameworkTables,
119
138
  }
120
139
  }
package/src/index.ts CHANGED
@@ -83,7 +83,7 @@ export type { AuditChainHead, AuditChainBreak, AuditChainBreakReason, AuditVerif
83
83
  export { jobs } from './async/jobs/namespace.js'
84
84
  export { JobAbortedError, JobLaterError, JobPermanentError } from './async/jobs/index.js'
85
85
  export { serializeError } from './async/jobs/serialize-error.js'
86
- export type { ExecuteJobOptions, Queue, QueueConfig, EnqueueOptions, ClaimedJob, JobClaim, JobClaimReason, ReapedJob, FailedAttempt, QueueTotals, QueueTypeTotals, JobResult, BackoffConfig,
86
+ export type { ExecuteJobOptions, Queue, QueueConfig, EnqueueOptions, ClaimedJob, JobClaim, JobClaimReason, ReapedJob, FailedAttempt, FailOptions, QueueTotals, QueueTypeTotals, JobResult, BackoffConfig,
87
87
  WaitOptions, WaitResult, ListenClient, ListenHandle, CronSchedule, ScheduleSpec,
88
88
  ScheduleInfo, SchedulerOptions } from './async/jobs/index.js'
89
89