Skip to content

feat(dbmanager): implement enterprise dbmanager and integrate with co… - #77

Merged
pramodnarayana merged 4 commits into
developmentfrom
feature/dbmanager
Mar 3, 2026
Merged

pramodnarayana merged 4 commits into
developmentfrom
feature/dbmanager

Conversation

@pramodnarayana

@pramodnarayana pramodnarayana commented Mar 3, 2026 •

Copy link
Copy Markdown
Owner

…ntrol plane

Summary by CodeRabbit

  • New Features

    • Lazy schema provisioning for leaner, faster connections
    • Triggers now auto-provision required gateway infrastructure when enabled
    • Introduced a declarative DB manager service used by the API to apply provisioning plans
  • Documentation

    • Added architecture guide on schema lifecycle and lazy provisioning
  • Tests

    • Updated tests to cover DB manager provisioning and failure scenarios

@coderabbitai

coderabbitai Bot commented Mar 3, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

New declarative dbmanager package and Nest module centralize schema provisioning (SchemaPlan/DatabaseManager). The API removes inline DDL, imports DbManagerModule, and services/tests now call dbManager.applyPlan for lazy namespace and gateway table provisioning.

Changes

Cohort / File(s) Summary
DbManager package
packages/dbmanager/package.json, packages/dbmanager/tsconfig.json, packages/dbmanager/src/interfaces.ts, packages/dbmanager/src/impl/sql-database-manager.ts, packages/dbmanager/src/index.ts
Adds new package exposing DatabaseManager and SchemaPlan, implements SqlDatabaseManager.applyPlan and provisionGatewayTables (schema validation, schema/table/index creation), build config, and re-exports.
API dependency & module
apps/api/package.json, apps/api/src/app/app.module.ts, apps/api/src/modules/dbmanager/dbmanager.module.ts
Adds workspace dependency @nexiom/dbmanager, registers a global DbManagerModule providing DB_MANAGER and imports it into AppModule.
Provisioning moved from inline SQL
apps/api/src/db/create-inbound-gateway.ts, packages/dbmanager/src/impl/sql-database-manager.ts
Removes legacy inline SQL helper file and migrates gateway provisioning into SqlDatabaseManager.provisionGatewayTables.
Service changes
apps/api/src/modules/connections/connectors.service.ts, apps/api/src/modules/trigger/trigger-executor.service.ts, apps/api/src/modules/trigger/trigger.module.ts
Injects DatabaseManager (DB_MANAGER); ConnectorsService returns schema name from TX then calls applyPlan(SchemaPlan.NAMESPACE_ONLY); TriggerExecutorService calls applyPlan(SchemaPlan.GATEWAY_ACTIVE) before enabling triggers and adds null-safe deterministic sorts; trigger.module updated to inject DB_MANAGER.
Tests updated
apps/api/src/modules/connections/connectors.service.spec.ts, apps/api/src/modules/trigger/trigger-executor.service.spec.ts
Adds mocked DB_MANAGER provider (applyPlan spy); connectors tests include failure scenario routed through dbManager.applyPlan; trigger tests inject mocked DatabaseManager.
Docs
docs/architecture/schema_lifecycle_and_provisioning.md
Adds design doc describing lazy provisioning, three-stage SchemaPlan, readiness layers, and the Ensure pattern for race conditions.
Misc
.gitignore
Adds *.tsbuildinfo to ignore list.

Sequence Diagram(s)

sequenceDiagram
    participant Trigger as TriggerExecutorService
    participant Conn as ConnectorsService
    participant DbMgr as DbManager<br/>(SqlDatabaseManager)
    participant Drizzle as DrizzleDb

    Conn->>Drizzle: begin tx, create workspace schema name (return schemaName)
    Conn-->>Conn: tx returns schemaName
    Conn->>DbMgr: applyPlan(schemaName, SchemaPlan.NAMESPACE_ONLY)
    DbMgr->>Drizzle: validate schemaName & CREATE SCHEMA IF NOT EXISTS
    Drizzle-->>DbMgr: schema created/existed
    Trigger->>DbMgr: applyPlan(schemaName, SchemaPlan.GATEWAY_ACTIVE)
    DbMgr->>Drizzle: CREATE TABLE inbound_gateway ... + CREATE INDEX ...
    Drizzle-->>DbMgr: table/indexes created
    DbMgr-->>Trigger: resolved
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 a quick nibble and a hop
Schemas whisper, "wait for a call",
DbManager wakes and builds the wall,
Tables sprout where none had been,
The rabbit applauds — tidy, lean!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: implementing a database manager module and integrating it with the API application, which is reflected across the file changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/dbmanager

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/modules/connections/connectors.service.spec.ts`:
- Around line 591-600: The test's ConfigService mock returns undefined for
get(), causing regionContext validation in ConnectorsService to throw before
applyPlan runs; update the TestingModule providers to supply a ConfigService
mock whose get() returns a valid regionContext object (matching the shape
checked by the service) so regionContext validation (the code around
regionContext in ConnectorsService) passes and the test can exercise applyPlan
and the failingDbManager path; keep the rest of the mocks (mockProviderRegistry,
mockEncryptionService, DATABASE_CONNECTION, DB_MANAGER) unchanged.

In `@apps/api/src/modules/connections/connectors.service.ts`:
- Line 345: The schemaName construction using `schemaName =
\`ws_${safeToken}_${hashedSuffix}\`` may approach PostgreSQL's 63-byte
identifier limit (max ~60 here) so add an explicit constraint/check: define a
constant like MAX_PG_IDENTIFIER_LENGTH = 63 (and a derived MAX_SCHEMA_NAME_LEN)
and ensure `safeToken` and/or `hashedSuffix` are truncated or validated before
composing `schemaName` (or throw a clear error); update the code paths that set
`safeToken`/`hashedSuffix` (references: `schemaName`, `safeToken`,
`hashedSuffix` in connectors.service) and add a short comment documenting the
length rationale.

In `@apps/api/src/modules/dbmanager/dbmanager.module.ts`:
- Around line 11-18: The factory's parameter drizzleDb in the provider for
DB_MANAGER should be explicitly typed to improve clarity and IDE support; import
the type alias (e.g., import type { DrizzleDb } from '@nexiom/database') and
update the useFactory signature to accept drizzleDb: DrizzleDb so the created
instance new SqlDatabaseManager(drizzleDb) has proper type information, leaving
the inject array (DATABASE_CONNECTION) unchanged.

In `@apps/api/src/modules/trigger/trigger-executor.service.ts`:
- Around line 441-445: The current explicit three-way comparator inside the
.sort((a, b) => { if (a < b) return -1; if (a > b) return 1; return 0; }) is
verbose—replace it with the idiomatic string comparator using localeCompare
(e.g., a.localeCompare(b)); if values may be null/undefined, normalize them
first (e.g., (a ?? '').localeCompare(b ?? '')) to avoid runtime errors; update
the comparator where it appears in trigger-executor.service.ts.

In `@docs/architecture/schema_lifecycle_and_provisioning.md`:
- Around line 18-24: The documentation incorrectly claims that provisioned
tables include sync_log, sync_cursor, and outbound_gateway while
SqlDatabaseManager.provisionGatewayTables() currently only creates
inbound_gateway; update the doc text in Stage 2 to reflect the actual behavior
by either (A) changing the "Tables Created" list to only include inbound_gateway
and adding a short note that sync_log, sync_cursor, and outbound_gateway are
planned additions, or (B) implement creation of the missing tables inside
SqlDatabaseManager.provisionGatewayTables() so the docs match the code;
reference SqlDatabaseManager.provisionGatewayTables() and the Stage 2 "Route
Activation (The \"Kernel\" Event)" section to locate where to change the docs or
the implementation.
- Around line 45-56: The documentation example is inconsistent: it calls
dbmanager.ensureKernelReady(schema) while the actual DatabaseManager API exposes
applyPlan(schemaName, plan); update the doc snippet in processLayerJob to call
the real method (e.g., dbmanager.applyPlan(schema, planOrEmpty) or show a small
wrapper method on DatabaseManager named ensureKernelReady that delegates to
applyPlan) so the doc matches the implementation and reference the symbols
processLayerJob, dbmanager.ensureKernelReady, and DatabaseManager.applyPlan when
making the change.

In `@packages/dbmanager/src/impl/sql-database-manager.ts`:
- Around line 23-26: The applyPlan flow can leave the schema partially created
if provisionGatewayTables fails; modify the function that runs CREATE SCHEMA and
calls provisionGatewayTables (e.g., applyPlan) to run all DDL inside an explicit
database transaction: begin a transaction via this.db.$client (e.g., BEGIN or
the client's transaction API), execute `CREATE SCHEMA IF NOT EXISTS
"${schemaName}";` and then call provisionGatewayTables and any subsequent DDL,
and on success COMMIT, otherwise ROLLBACK on error; ensure you use the same
client/connection for the whole transaction and surface the original error after
rollback.
- Around line 39-43: The code currently returns early when plan ===
SchemaPlan.REPLICA_ACTIVE, causing replica tables to never be provisioned;
update the logic in sql-database-manager (the method handling SchemaPlan) to
either call the intended provisionReplicaTables(schemaName) implementation
before returning or, if that implementation isn’t ready, throw a clear,
descriptive Error when SchemaPlan.REPLICA_ACTIVE is requested so callers don’t
get a silent success; reference SchemaPlan.REPLICA_ACTIVE and
provisionReplicaTables(schemaName) (or the containing method name if you prefer)
and ensure the error message states that REPLICA_ACTIVE provisioning is not yet
implemented.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d6e70f9 and 9d6005f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (16)
  • apps/api/package.json
  • apps/api/src/app/app.module.ts
  • apps/api/src/db/create-inbound-gateway.ts
  • apps/api/src/modules/connections/connectors.service.spec.ts
  • apps/api/src/modules/connections/connectors.service.ts
  • apps/api/src/modules/dbmanager/dbmanager.module.ts
  • apps/api/src/modules/trigger/trigger-executor.service.spec.ts
  • apps/api/src/modules/trigger/trigger-executor.service.ts
  • apps/api/src/modules/trigger/trigger.module.ts
  • docs/architecture/schema_lifecycle_and_provisioning.md
  • packages/dbmanager/package.json
  • packages/dbmanager/src/impl/sql-database-manager.ts
  • packages/dbmanager/src/index.ts
  • packages/dbmanager/src/interfaces.ts
  • packages/dbmanager/tsconfig.json
  • packages/dbmanager/tsconfig.tsbuildinfo
💤 Files with no reviewable changes (1)
  • apps/api/src/db/create-inbound-gateway.ts

Comment on lines +591 to +600
const moduleFail: TestingModule = await Test.createTestingModule({
providers: [
ConnectorsService,
{ provide: ProviderRegistryService, useValue: mockProviderRegistry },
{ provide: ConfigService, useValue: { get: vi.fn() } },
{ provide: EncryptionService, useValue: mockEncryptionService },
{ provide: DATABASE_CONNECTION, useValue: mockDb as unknown },
{ provide: DB_MANAGER, useValue: failingDbManager },
],
}).compile();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Test will fail due to missing regionContext mock.

The ConfigService mock at line 595 returns undefined for all get() calls, causing the regionContext validation (lines 290-298 in the service) to throw InternalServerErrorException before applyPlan is ever called. This means the test isn't actually exercising the applyPlan failure path.

🐛 Proposed fix
         { provide: ProviderRegistryService, useValue: mockProviderRegistry },
-        { provide: ConfigService, useValue: { get: vi.fn() } },
+        { provide: ConfigService, useValue: { get: vi.fn().mockReturnValue('us-east-1') } },
         { provide: EncryptionService, useValue: mockEncryptionService },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const moduleFail: TestingModule = await Test.createTestingModule({
providers: [
ConnectorsService,
{ provide: ProviderRegistryService, useValue: mockProviderRegistry },
{ provide: ConfigService, useValue: { get: vi.fn() } },
{ provide: EncryptionService, useValue: mockEncryptionService },
{ provide: DATABASE_CONNECTION, useValue: mockDb as unknown },
{ provide: DB_MANAGER, useValue: failingDbManager },
],
}).compile();
const moduleFail: TestingModule = await Test.createTestingModule({
providers: [
ConnectorsService,
{ provide: ProviderRegistryService, useValue: mockProviderRegistry },
{ provide: ConfigService, useValue: { get: vi.fn().mockReturnValue('us-east-1') } },
{ provide: EncryptionService, useValue: mockEncryptionService },
{ provide: DATABASE_CONNECTION, useValue: mockDb as unknown },
{ provide: DB_MANAGER, useValue: failingDbManager },
],
}).compile();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/modules/connections/connectors.service.spec.ts` around lines 591
- 600, The test's ConfigService mock returns undefined for get(), causing
regionContext validation in ConnectorsService to throw before applyPlan runs;
update the TestingModule providers to supply a ConfigService mock whose get()
returns a valid regionContext object (matching the shape checked by the service)
so regionContext validation (the code around regionContext in ConnectorsService)
passes and the test can exercise applyPlan and the failingDbManager path; keep
the rest of the mocks (mockProviderRegistry, mockEncryptionService,
DATABASE_CONNECTION, DB_MANAGER) unchanged.

const finalProviderToken = sanitizedProvider || 'unknown';
const safeToken = finalProviderToken.substring(0, 40);
const workspaceSchemaName = `ws_${safeToken}_${hashedSuffix}`;
const schemaName = `ws_${safeToken}_${hashedSuffix}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Verify schema name length constraints.

The generated schemaName pattern ws_${safeToken}_${hashedSuffix} could be up to 3 + 40 + 1 + 16 = 60 characters. PostgreSQL limits identifier names to 63 bytes by default, so this is safe, but worth confirming the constraint is intentional.

📝 Consider adding a constant for documentation
+        // Max schema name: ws_ (3) + provider (40) + _ (1) + hash (16) = 60 chars (< PG 63-byte limit)
         const schemaName = `ws_${safeToken}_${hashedSuffix}`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const schemaName = `ws_${safeToken}_${hashedSuffix}`;
// Max schema name: ws_ (3) + provider (40) + _ (1) + hash (16) = 60 chars (< PG 63-byte limit)
const schemaName = `ws_${safeToken}_${hashedSuffix}`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/modules/connections/connectors.service.ts` at line 345, The
schemaName construction using `schemaName = \`ws_${safeToken}_${hashedSuffix}\``
may approach PostgreSQL's 63-byte identifier limit (max ~60 here) so add an
explicit constraint/check: define a constant like MAX_PG_IDENTIFIER_LENGTH = 63
(and a derived MAX_SCHEMA_NAME_LEN) and ensure `safeToken` and/or `hashedSuffix`
are truncated or validated before composing `schemaName` (or throw a clear
error); update the code paths that set `safeToken`/`hashedSuffix` (references:
`schemaName`, `safeToken`, `hashedSuffix` in connectors.service) and add a short
comment documenting the length rationale.

Comment on lines +362 to +368
// 3. Apply the initial schema plan (NAMESPACE_ONLY) outside the transaction
// so the DDL runs on its own connection and does not silently escape the
// Drizzle tx scope (db.$client vs the transaction's dedicated connection).
await this.dbManager.applyPlan(
workspaceSchemaName,
SchemaPlan.NAMESPACE_ONLY,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's understand the structure of the connectors.service.ts file
fd connectors.service.ts --type f

Repository: pramodnarayana/nexiom

Length of output: 119


🏁 Script executed:

# Search for the storeOAuthConnection function and surrounding context
rg -n "storeOAuthConnection" --type=ts -A 70 | head -100

Repository: pramodnarayana/nexiom

Length of output: 9517


🏁 Script executed:

# Search for applyPlan function definition
rg -n "applyPlan\s*\(" --type=ts -B 2 -A 10

Repository: pramodnarayana/nexiom

Length of output: 4011


🏁 Script executed:

# Search for PENDING_PROVISIONING or similar provisioning statuses
rg -n "PENDING_PROVISIONING|ProvisioningStatus|ConnectionStatus" --type=ts

Repository: pramodnarayana/nexiom

Length of output: 1328


🏁 Script executed:

# Check for retry mechanisms and error handling patterns
rg -n "retry|Retry|RETRY" --type=ts -A 3 | head -80

Repository: pramodnarayana/nexiom

Length of output: 6145


🏁 Script executed:

# Search for connectionStorageRegistry definition and usage
rg -n "connectionStorageRegistry" --type=ts -B 2 -A 5 | head -100

Repository: pramodnarayana/nexiom

Length of output: 9413


🏁 Script executed:

# Get the full storeOAuthConnection function to see complete error handling
sed -n '278,385p' apps/api/src/modules/connections/connectors.service.ts

Repository: pramodnarayana/nexiom

Length of output: 3523


🏁 Script executed:

# Check the full AppConnectionStatus enum definition
cat packages/database/src/schema/tenant.ts | head -30

Repository: pramodnarayana/nexiom

Length of output: 1323


🏁 Script executed:

# Search for background jobs or reconciliation mechanisms
rg -n "reconcil|background|cron|job|cleanup" --type=ts -i | grep -E "(reconcil|background|cron|cleanup)" | head -40

Repository: pramodnarayana/nexiom

Length of output: 4646


🏁 Script executed:

# Check if there's any mechanism to handle orphaned connections
rg -n "orphan|dangling|incomplete|unprovisioned" --type=ts -i

Repository: pramodnarayana/nexiom

Length of output: 1251


🏁 Script executed:

# Look for error recovery or compensating transaction patterns
rg -n "compensat|rollback|cleanup|recover" --type=ts -i | head -30

Repository: pramodnarayana/nexiom

Length of output: 2937


Fix partial failure vulnerability: applyPlan failure after transaction commits orphans metadata.

If applyPlan fails after the transaction commits, appConnections will be persisted with ACTIVE status while the schema remains unprovisioned. The system exposes this inconsistency with no recovery mechanism.

Implement one of:

  1. Compensating transaction: Delete the connectionStorageRegistry row on applyPlan failure
  2. Retry with backoff: Retry applyPlan before throwing (it is idempotent via CREATE SCHEMA IF NOT EXISTS)
  3. Provisioning status: Add a PENDING_SCHEMA status to AppConnectionStatus and mark the connection during provisioning, with a background reconciler to clean up stale entries

Comment thread apps/api/src/modules/dbmanager/dbmanager.module.ts
Comment thread apps/api/src/modules/trigger/trigger-executor.service.ts Outdated
Comment thread docs/architecture/schema_lifecycle_and_provisioning.md Outdated
Comment thread docs/architecture/schema_lifecycle_and_provisioning.md
Comment thread packages/dbmanager/src/impl/sql-database-manager.ts
Comment thread packages/dbmanager/src/impl/sql-database-manager.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/src/modules/connections/connectors.service.spec.ts (1)

566-619: 🧹 Nitpick | 🔵 Trivial

Assert applyPlan call contract in the failure-path test.

The test checks thrown error but not that DB_MANAGER.applyPlan was called with the expected plan. Add explicit call assertions to ensure this path is exercised as intended.

✅ Suggested test hardening
 import { DB_MANAGER } from '../dbmanager/dbmanager.module';
+import { SchemaPlan } from '@nexiom/dbmanager';
@@
       await expect(
         failingService.storeOAuthConnection({
@@
         }),
       ).rejects.toThrow(InternalServerErrorException);
+
+      expect(failingDbManager.applyPlan).toHaveBeenCalledTimes(1);
+      expect(failingDbManager.applyPlan).toHaveBeenCalledWith(
+        expect.stringMatching(/^ws_[a-z0-9_]+$/),
+        SchemaPlan.NAMESPACE_ONLY,
+      );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/modules/connections/connectors.service.spec.ts` around lines 566
- 619, Add assertions that DB_MANAGER.applyPlan was invoked with the expected
plan and invocation count in this failure-path test: after calling
failingService.storeOAuthConnection(...).rejects.toThrow(...), assert the
failingDbManager.applyPlan mock was called once (toHaveBeenCalledTimes(1)) and
that it received the expected plan shape using
toHaveBeenCalledWith/expect.objectContaining (e.g.,
expect(failingDbManager.applyPlan).toHaveBeenCalledWith(expect.objectContaining({
operations: expect.any(Array) }) or the exact plan structure your implementation
builds). Reference the failingDbManager mock, DB_MANAGER.applyPlan, and
ConnectorsService.storeOAuthConnection when adding these assertions.
♻️ Duplicate comments (1)
apps/api/src/modules/connections/connectors.service.ts (1)

302-368: ⚠️ Potential issue | 🟠 Major

Avoid committing ACTIVE metadata before provisioning succeeds.

The transaction commits appConnections.status = ACTIVE and storage registry rows before applyPlan runs. If applyPlan fails, connection metadata remains active while infrastructure is not provisioned.

Use a two-phase flow (provisioning status → ACTIVE on success) or add compensating failure handling so persisted state cannot claim readiness prematurely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/modules/connections/connectors.service.ts` around lines 302 -
368, The code currently upserts appConnections with status ACTIVE inside
db.transaction before calling this.dbManager.applyPlan, which may mark the
connection ready even if applyPlan fails; modify the flow to use a two-phase
state: during the initial tx (in the block using db.transaction where
appConnections is upserted and connectionStorageRegistry is inserted) set the
connection status to a provisional state such as PROVISIONING or PENDING
(instead of AppConnectionStatus.ACTIVE) and return schemaName; then call
this.dbManager.applyPlan(workspaceSchemaName, SchemaPlan.NAMESPACE_ONLY) outside
the transaction and only after it succeeds perform a separate update (e.g.
update appConnections by id) to set status = AppConnectionStatus.ACTIVE (and
updatedAt); additionally, on applyPlan failure run a compensating
rollback/update (e.g. mark appConnections as FAILED or remove the
connectionStorageRegistry row) to avoid leaving the record in a ready
state—refer to appConnections, connectionStorageRegistry, db.transaction, and
applyPlan when locating the changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/architecture/schema_lifecycle_and_provisioning.md`:
- Line 56: The "1. Benefits of this Timing" heading restarts numbering; update
the ordered list item label to continue the sequence (change "1." to "5.") so
the section follows the previous "4." heading; look for the heading text
"Benefits of this Timing" in the document and adjust its numeric prefix
accordingly.

---

Outside diff comments:
In `@apps/api/src/modules/connections/connectors.service.spec.ts`:
- Around line 566-619: Add assertions that DB_MANAGER.applyPlan was invoked with
the expected plan and invocation count in this failure-path test: after calling
failingService.storeOAuthConnection(...).rejects.toThrow(...), assert the
failingDbManager.applyPlan mock was called once (toHaveBeenCalledTimes(1)) and
that it received the expected plan shape using
toHaveBeenCalledWith/expect.objectContaining (e.g.,
expect(failingDbManager.applyPlan).toHaveBeenCalledWith(expect.objectContaining({
operations: expect.any(Array) }) or the exact plan structure your implementation
builds). Reference the failingDbManager mock, DB_MANAGER.applyPlan, and
ConnectorsService.storeOAuthConnection when adding these assertions.

---

Duplicate comments:
In `@apps/api/src/modules/connections/connectors.service.ts`:
- Around line 302-368: The code currently upserts appConnections with status
ACTIVE inside db.transaction before calling this.dbManager.applyPlan, which may
mark the connection ready even if applyPlan fails; modify the flow to use a
two-phase state: during the initial tx (in the block using db.transaction where
appConnections is upserted and connectionStorageRegistry is inserted) set the
connection status to a provisional state such as PROVISIONING or PENDING
(instead of AppConnectionStatus.ACTIVE) and return schemaName; then call
this.dbManager.applyPlan(workspaceSchemaName, SchemaPlan.NAMESPACE_ONLY) outside
the transaction and only after it succeeds perform a separate update (e.g.
update appConnections by id) to set status = AppConnectionStatus.ACTIVE (and
updatedAt); additionally, on applyPlan failure run a compensating
rollback/update (e.g. mark appConnections as FAILED or remove the
connectionStorageRegistry row) to avoid leaving the record in a ready
state—refer to appConnections, connectionStorageRegistry, db.transaction, and
applyPlan when locating the changes.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9d6005f and 96fb163.

📒 Files selected for processing (7)
  • apps/api/src/modules/connections/connectors.service.spec.ts
  • apps/api/src/modules/connections/connectors.service.ts
  • apps/api/src/modules/dbmanager/dbmanager.module.ts
  • apps/api/src/modules/trigger/trigger-executor.service.ts
  • docs/architecture/schema_lifecycle_and_provisioning.md
  • packages/dbmanager/src/impl/sql-database-manager.ts
  • packages/dbmanager/tsconfig.tsbuildinfo

await db.withSchema(schema).insert(...);
}

1. Benefits of this Timing

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix section numbering in the heading.

Line 56 resets numbering to 1. after section 4. This should continue the sequence (likely 5.) to avoid confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/architecture/schema_lifecycle_and_provisioning.md` at line 56, The "1.
Benefits of this Timing" heading restarts numbering; update the ordered list
item label to continue the sequence (change "1." to "5.") so the section follows
the previous "4." heading; look for the heading text "Benefits of this Timing"
in the document and adjust its numeric prefix accordingly.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
packages/dbmanager/src/impl/sql-database-manager.ts (2)

39-43: ⚠️ Potential issue | 🟠 Major

REPLICA_ACTIVE still returns without replica provisioning.

Line 41–Line 42 reports success but does not create replica structures. This can cause deferred runtime failures in callers expecting replica readiness.

🛠️ Proposed fix
         // 3. (Future) Ensure Replica Tables exist
         // TODO: implement provisionReplicaTables(schemaName) before enabling REPLICA_ACTIVE in production
         if (plan === SchemaPlan.REPLICA_ACTIVE) {
-            return;
+            throw new Error(
+                'SchemaPlan.REPLICA_ACTIVE is not implemented yet. Use SchemaPlan.GATEWAY_ACTIVE until replica provisioning is available.',
+            );
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/dbmanager/src/impl/sql-database-manager.ts` around lines 39 - 43,
The branch that exits early when plan === SchemaPlan.REPLICA_ACTIVE must
actually provision replica structures instead of returning; update the block in
sql-database-manager.ts to call an implementation like
provisionReplicaTables(schemaName) (implement that function if missing) and
await it before returning, propagate/log any errors and only return after
successful provisioning so callers can rely on replica readiness; reference the
SchemaPlan.REPLICA_ACTIVE check and the new provisionReplicaTables(schemaName)
call.

23-33: ⚠️ Potential issue | 🟠 Major

Provisioning flow is non-atomic across multiple DDL statements.

If any later DDL fails after earlier statements succeed, the schema is left partially provisioned (despite the comment around Line 47–Line 49). Wrap the full plan execution in an explicit transaction.

🛠️ Proposed fix
     async applyPlan(schemaName: string, plan: SchemaPlan): Promise<void> {
         this.validateSchemaName(schemaName);
+        await this.db.$client.query('BEGIN');
+        try {
 
-        // 1. Always ensure namespace exists (Minimum baseline for all plans)
-        await this.db.$client.query(
-            `CREATE SCHEMA IF NOT EXISTS "${schemaName}";`,
-        );
+            // 1. Always ensure namespace exists (Minimum baseline for all plans)
+            await this.db.$client.query(
+                `CREATE SCHEMA IF NOT EXISTS "${schemaName}";`,
+            );
 
-        if (plan === SchemaPlan.NAMESPACE_ONLY) {
-            return;
-        }
+            if (plan === SchemaPlan.NAMESPACE_ONLY) {
+                await this.db.$client.query('COMMIT');
+                return;
+            }
 
-        // 2. Ensure Gateway Tables exist
-        await this.provisionGatewayTables(schemaName);
+            // 2. Ensure Gateway Tables exist
+            await this.provisionGatewayTables(schemaName);
 
-        if (plan === SchemaPlan.GATEWAY_ACTIVE) {
-            return;
-        }
+            if (plan === SchemaPlan.GATEWAY_ACTIVE) {
+                await this.db.$client.query('COMMIT');
+                return;
+            }
 
-        // 3. (Future) Ensure Replica Tables exist
-        // TODO: implement provisionReplicaTables(schemaName) before enabling REPLICA_ACTIVE in production
-        if (plan === SchemaPlan.REPLICA_ACTIVE) {
-            return;
-        }
+            // 3. (Future) Ensure Replica Tables exist
+            // TODO: implement provisionReplicaTables(schemaName) before enabling REPLICA_ACTIVE in production
+            if (plan === SchemaPlan.REPLICA_ACTIVE) {
+                throw new Error(
+                    'SchemaPlan.REPLICA_ACTIVE is not implemented yet.',
+                );
+            }
+            await this.db.$client.query('COMMIT');
+        } catch (error) {
+            await this.db.$client.query('ROLLBACK');
+            throw error;
+        }
     }

Also applies to: 46-79

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/dbmanager/src/impl/sql-database-manager.ts` around lines 23 - 33,
The provisioning flow executes multiple DDLs outside a transaction, so wrap the
entire plan in an explicit DB transaction: begin a transaction via
this.db.$client (or its transaction helper), then run the CREATE SCHEMA query
and the subsequent steps (including the check for SchemaPlan.NAMESPACE_ONLY and
the call to this.provisionGatewayTables(schemaName)), commit on success and
rollback on any error, rethrow the error after rollback; ensure the early return
for SchemaPlan.NAMESPACE_ONLY happens inside the transaction so the transaction
can be committed/rolled back appropriately.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/dbmanager/src/impl/sql-database-manager.ts`:
- Around line 39-43: The branch that exits early when plan ===
SchemaPlan.REPLICA_ACTIVE must actually provision replica structures instead of
returning; update the block in sql-database-manager.ts to call an implementation
like provisionReplicaTables(schemaName) (implement that function if missing) and
await it before returning, propagate/log any errors and only return after
successful provisioning so callers can rely on replica readiness; reference the
SchemaPlan.REPLICA_ACTIVE check and the new provisionReplicaTables(schemaName)
call.
- Around line 23-33: The provisioning flow executes multiple DDLs outside a
transaction, so wrap the entire plan in an explicit DB transaction: begin a
transaction via this.db.$client (or its transaction helper), then run the CREATE
SCHEMA query and the subsequent steps (including the check for
SchemaPlan.NAMESPACE_ONLY and the call to
this.provisionGatewayTables(schemaName)), commit on success and rollback on any
error, rethrow the error after rollback; ensure the early return for
SchemaPlan.NAMESPACE_ONLY happens inside the transaction so the transaction can
be committed/rolled back appropriately.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 96fb163 and 9df4104.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • .gitignore
  • packages/dbmanager/src/impl/sql-database-manager.ts

@pramodnarayana
pramodnarayana merged commit b4f7fe0 into development Mar 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant