Add ChronoPostgresDatastore implementation#59
Conversation
- Implement PostgreSQL datastore using TypeORM with raw SQL for atomic task claiming - Use FOR UPDATE SKIP LOCKED pattern for concurrent task claiming - Add PGLite-based tests that run without external database dependency - Fix tsdown config across packages to use hash: false for clean output filenames Co-Authored-By: Claude (claude-4-opus) <noreply@anthropic.com>
Wiz Scan Summary
To detect these findings earlier in the dev lifecycle, try using Wiz Code VS Code Extension. |
| @@ -0,0 +1,550 @@ | |||
| import { PGlite } from '@electric-sql/pglite'; | |||
There was a problem hiding this comment.
New dev dependency
- Replace all string interpolation SQL with QueryBuilder methods - Use Brackets for complex OR conditions in claim query - Use transaction with pessimistic_write lock for atomic claiming - Update test mock to support QueryBuilder API Co-Authored-By: Claude (claude-4-opus) <noreply@anthropic.com>
Move mock QueryBuilder and DataSource infrastructure from test file to test/helpers/mock-datasource.ts to improve test file readability. Co-Authored-By: Claude (claude-4-opus) <noreply@anthropic.com>
Split the dense mock-datasource.ts into focused, well-documented modules: - sql-utils.ts: SQL string manipulation (snake_case, params) - entity-mapper.ts: Database row to entity conversion - where-clause-builder.ts: WHERE clause construction - mock-query-builder.ts: TypeORM QueryBuilder simulation - mock-entity-manager.ts: TypeORM EntityManager simulation - mock-datasource.ts: Main DataSource entry point - index.ts: Re-exports with architecture diagram Each file has clear documentation and single responsibility. Co-Authored-By: Claude (claude-4-opus) <noreply@anthropic.com>
| @@ -0,0 +1,64 @@ | |||
| /** | |||
There was a problem hiding this comment.
I'm using PGLite for in memory postgres testing.
AI generated test magic ahead. This is significantly more complicated due to supporting the ORMs query builder. The other option is allowing string interpolation in the source, pick your poison, this seems better to me.
There was a problem hiding this comment.
Ditched PGLite. Doesn't really work with typeORM
e56683f to
b5d361f
Compare
- Remove PGlite and mock helpers in favor of real Postgres integration tests - Fix RETURNING clause mapping: raw PostgreSQL rows use snake_case columns, added fromRaw() helper to convert to camelCase entity properties - Add CI workflow with Postgres 15 service container - Add testAccessor Symbol for testing query generation - Fix setLock syntax from array-based to method chaining Co-Authored-By: Claude (claude-4-opus) <noreply@anthropic.com>
b5d361f to
c7b8ca4
Compare
| @@ -13,6 +13,21 @@ jobs: | |||
| runs-on: ubuntu-latest | |||
| timeout-minutes: 10 | |||
|
|
|||
There was a problem hiding this comment.
I looked into alternatives to testing with real postgres. I tried PGLite but it was not good with TypeORM and defeated the purpose of the tests.
Query generation tests are no longer needed - actual cleanup behavior is already verified by the integration tests. Co-Authored-By: Claude (claude-4-opus) <noreply@anthropic.com>
Co-Authored-By: Claude (claude-4-opus) <noreply@anthropic.com>
New tests: - Concurrent claim safety (SKIP LOCKED verification) - Deferred initialization waiting behavior - EntityManager transaction participation - Claim skips COMPLETED/FAILED tasks - Claim skips non-stale claimed tasks - Retry clears claimedAt - Delete by kind+idempotencyKey edge cases (non-existent, with force) - Cleanup preserves PENDING and FAILED tasks Co-Authored-By: Claude (claude-4-opus) <noreply@anthropic.com>
This fixes TypeORM version mismatch issues when the package is used as a dependency. The consumer's TypeORM version will be used instead of bundling a separate copy. Bump to 0.5.2-alpha.0 for testing. Co-Authored-By: Claude (claude-4-opus) <noreply@anthropic.com>
…t silent failures Add Promise.race with configurable timeout to getDataSource() method. If the datastore is not initialized within the timeout period (default 10s), operations will throw an error instead of hanging indefinitely. This prevents silent failures when multiple datastore instances are created and some are never initialized (e.g., due to DI misconfiguration). Co-Authored-By: Claude (claude-4-opus) <noreply@anthropic.com>
- Replace non-null assertions with optional chaining in tests - Pass DataSource as parameter to cleanup methods to avoid non-null assertions - Apply biome formatting fixes Co-Authored-By: Claude (claude-4-opus) <noreply@anthropic.com>
darrenpicard25
left a comment
There was a problem hiding this comment.
looks good. just some discussion points
| const entity = manager.create(ChronoTaskEntity, { | ||
| kind: String(input.kind), | ||
| status: TaskStatus.PENDING, | ||
| data: input.data as Record<string, unknown>, |
There was a problem hiding this comment.
booo to cast despite mongo package doing same thing
| } | ||
|
|
||
| const result = await qb.execute(); | ||
| const [rawRow] = result.raw as Record<string, unknown>[]; |
| }); | ||
|
|
||
| // Opportunistic cleanup runs after claim completes, outside the transaction | ||
| this.maybeCleanupCompletedTasks(); |
There was a problem hiding this comment.
i get it but slightly dislike this clean up logic being apart of claim flow
There was a problem hiding this comment.
can we solve this with postgres built in pub/sub model
There was a problem hiding this comment.
Opportunistic cleanup is simpler. LISTEN/NOTIFY adds complexity for marginal benefit.
The AI agrees with me here, but we can discuss more with the group.
There was a problem hiding this comment.
yeah lets discuss today
packages/chrono-postgres-datastore/src/chrono-postgres-datastore.ts
Outdated
Show resolved
Hide resolved
lastExecutedAt should only be set when task execution finishes (complete/fail), not when scheduling a retry. This aligns with the mongo datastore behavior. Co-Authored-By: Claude (claude-4-opus) <noreply@anthropic.com>
BREAKING CHANGE: Replace TypeORM DataSource with pg Pool interface - Replace initialize(dataSource: DataSource) with initialize(pool: Pool) - Replace datastoreOptions.entityManager with datastoreOptions.client (PoolClient) - Remove ChronoTaskEntity, add ChronoTaskRow type - Export migration SQL (MIGRATION_UP_SQL, MIGRATION_DOWN_SQL) and helpers - Use prepared statements for all queries - Remove typeorm peer dependency, add pg as direct dependency This simplifies the implementation and removes the heavy TypeORM dependency in favor of the lightweight pg library with explicit SQL queries. Co-Authored-By: Claude (claude-4-opus) <noreply@anthropic.com>
Combine SELECT+UPDATE and SELECT+DELETE into single atomic statements: - claim(): Single UPDATE with subquery replaces transaction with separate SELECT FOR UPDATE SKIP LOCKED + UPDATE - cleanup(): Single DELETE with subquery replaces SELECT IDs + DELETE This reduces round trips and removes explicit transaction management for claim operations. Co-Authored-By: Claude (claude-4-opus) <noreply@anthropic.com>
- Add test/tsconfig.json to enable typechecking test files - Update package.json typecheck script to include test directory - Fix TypeScript strict mode errors in tests Co-Authored-By: Claude (claude-4-opus) <noreply@anthropic.com>
| const timeoutPromise = new Promise<never>((_, reject) => { | ||
| setTimeout(() => { | ||
| reject( | ||
| new Error( | ||
| `ChronoPostgresDatastore initialization timeout: datastore was not initialized within ${this.config.initializationTimeoutMs}ms. ` + | ||
| 'Ensure initialize() is called before performing operations.', | ||
| ), | ||
| ); | ||
| }, this.config.initializationTimeoutMs); | ||
| }); | ||
|
|
||
| return Promise.race([initPromise, timeoutPromise]); | ||
| } |
There was a problem hiding this comment.
@darrenpicard25 As we are doing something similar in chrono-mongo-datastore, is there a race condition between the initialization promise resolving and the first messages that the service starts to publish? Because in ALWAYS mode this would mean losing messages right?
There was a problem hiding this comment.
not really a race problem. just more a problem with the datastore never being initialized
darrenpicard25
left a comment
There was a problem hiding this comment.
looking good. few minor things and we will see what people thing in todays meeting
| @@ -0,0 +1,8 @@ | |||
| { | |||
| "extends": "../../../tsconfig.base.json", | |||
There was a problem hiding this comment.
would recommend just extending the chrono-postgres-datastore tsconfig not the base
| }, | ||
| "require": { | ||
| "types": "./build/index.d.ts", | ||
| "default": "./build/index.js" |
| "@types/pg": "^8.11.0" | ||
| }, | ||
| "peerDependencies": { | ||
| "@neofinancial/chrono": ">=0.5.0" |
There was a problem hiding this comment.
| "@neofinancial/chrono": ">=0.5.0" | |
| "@neofinancial/chrono": "workspace:*" |
for now.
| "isolatedModules": true, | ||
| "verbatimModuleSyntax": false, | ||
| "declaration": true, | ||
| "declarationMap": true, |
| type PostgresDatastoreOptions, | ||
| } from './chrono-postgres-datastore'; | ||
| export { MIGRATION_DOWN_SQL, MIGRATION_UP_SQL, migrateDown, migrateUp } from './migration'; | ||
| export type { ChronoTaskRow } from './types'; |
There was a problem hiding this comment.
should this be exported? maybe. dont actually know. but this would be the equivalent of mongo-datastore exporting the Document type
| input: ScheduleInput<TaskKind, TaskMapping[TaskKind], PostgresDatastoreOptions>, | ||
| ): Promise<Task<TaskKind, TaskMapping[TaskKind]>> { | ||
| await this.getPool(); | ||
| const queryable = this.getQueryable(input.datastoreOptions); |
There was a problem hiding this comment.
huh? we are doing the same things twice but with different behaviour
| query = DELETE_BY_KEY_QUERY; | ||
| values = [String(key.kind), key.idempotencyKey, TaskStatus.PENDING]; | ||
| } | ||
| } |
There was a problem hiding this comment.
would prefer to see in some sort of "getDeleteTaskQuery" private method over if blocks with let variables
| }); | ||
|
|
||
| // Opportunistic cleanup runs after claim completes, outside the transaction | ||
| this.maybeCleanupCompletedTasks(); |
There was a problem hiding this comment.
yeah lets discuss today
| id: row.id, | ||
| kind: row.kind as TaskKind, | ||
| status: row.status as TaskStatus, | ||
| data: row.data as TaskMapping[TaskKind], |
There was a problem hiding this comment.
booo to casting. Feel like this should not be needed based on everything ive seen
| ]; | ||
|
|
||
| try { | ||
| const result = await queryable.query<ChronoTaskRow>(SCHEDULE_QUERY, values); |
There was a problem hiding this comment.
not a huge deal but would be nice to enforce that values aligns with the expected types for the query. might not be possible but would be nice
- Made getQueryable async so it handles initialization internally - Updated all public methods (schedule, delete, claim, retry, complete, fail) to use getQueryable consistently instead of mixing getPool() + pool.query() - Simplified schedule() by removing redundant getPool() call - getQueryable now properly awaits pool initialization via getPool() Co-Authored-By: Claude (claude-4-opus) <noreply@anthropic.com>
Address PR review feedback to use a more current postgres version in CI. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
More Details
Vulnerabilities [minimatch:10.0.1]
| Name | Severity | Source | Fixed version | CVSS score | CVSS exploitability score | Has public exploit | Has CISA KEV exploit |
|---|---|---|---|---|---|---|---|
| CVE-2026-26996 | GHSA-3ppc-4f35-3m26 | 10.2.1 | 8.7 | 3.9 | true | false |
To ignore this finding as an exception, reply to this conversation with #wiz_ignore reason
If you'd like to ignore this finding in all future scans, add an exception in the .wiz file (learn more) or create an Ignore Rule (learn more).
To get more details on how to remediate this issue using AI, reply to this conversation with #wiz remediate
There was a problem hiding this comment.
More Details
Vulnerabilities [glob:11.0.1]
| Name | Severity | Source | Fixed version | CVSS score | CVSS exploitability score | Has public exploit | Has CISA KEV exploit |
|---|---|---|---|---|---|---|---|
| CVE-2025-64756 | GHSA-5j98-mcp5-4vw2 | 11.1.0 | 7.5 | 1.6 | true | false |
To ignore this finding as an exception, reply to this conversation with #wiz_ignore reason
If you'd like to ignore this finding in all future scans, add an exception in the .wiz file (learn more) or create an Ignore Rule (learn more).
To get more details on how to remediate this issue using AI, reply to this conversation with #wiz remediate
Description
Tested in service: