Skip to content
This repository was archived by the owner on Jul 21, 2026. It is now read-only.

feat(memory): add new adapter - #36

Merged
almeidazs merged 4 commits into
mainfrom
feat/memory-adapter
Jun 11, 2026
Merged

feat(memory): add new adapter#36
almeidazs merged 4 commits into
mainfrom
feat/memory-adapter

Conversation

@almeidazs

@almeidazs almeidazs commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced @paymesh/memory, an in-memory database adapter for ephemeral data persistence
    • Memory adapter is recommended for tests, CI, demos, and local development
    • CLI commands now recognize and appropriately handle the memory adapter
    • CLI status command now displays the active database adapter
  • Documentation

    • Updated installation and guide documentation to include the memory adapter
    • Added comprehensive memory adapter documentation with usage examples

@almeidazs almeidazs self-assigned this Jun 11, 2026
@almeidazs almeidazs added the enhancement New feature or request label Jun 11, 2026
@vercel

vercel Bot commented Jun 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
paymesh Ready Ready Preview, Comment Jun 11, 2026 6:11pm

@almeidazs almeidazs linked an issue Jun 11, 2026 that may be closed by this pull request
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8d508f89-bdd3-40d2-8314-9dd6601c5352

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces @paymesh/memory, a first-party in-memory ephemeral database adapter. It includes the complete adapter implementation with state management and repositories, CLI command awareness (early-exit for migrations, adapter reporting in status), comprehensive documentation, and test coverage for all core behaviors.

Changes

In-Memory Database Adapter Implementation

Layer / File(s) Summary
Type definitions and schema utilities
packages/memory/src/types.ts, packages/memory/src/state.ts, packages/memory/src/shared/schema.ts, packages/memory/src/shared/raw.ts, packages/memory/src/shared/cursor.ts
Seed record types, stored entity interfaces, state shape, and utility functions for schema field defaults/validation, raw payload handling, cloning, and keyset-based customer list pagination with base64url cursor encoding/decoding.
Driver factory and repository implementations
packages/memory/src/shared/driver.ts, packages/memory/src/repositories.ts
createMemoryDriver wires the database adapter contract with repositories backed by in-memory Maps. Repositories implement customers (with cursor pagination), payments (pix/checkouts/invoices), subscriptions (with reference validation), webhook events (with duplicate detection), products, prices, and migrations tracking, plus shared payment validation and mapper helpers.
Seeding and referential integrity
packages/memory/src/state.ts
applySeed populates initial state; per-entity insert functions validate required fields, apply schema defaults, enforce uniqueness, and in strict mode validate referenced entities. Transactional isolation via state cloning.
Public API and package configuration
packages/memory/src/index.ts, packages/memory/package.json, packages/memory/tsconfig.json, packages/memory/tsdown.config.mjs, packages/memory/README.md
memory() factory initializes state and wiring; dual ESM/CJS package build; TypeScript and bundler config; package-level documentation.
Test suite
packages/memory/test/memory.test.ts
Seeded data access, customer pagination, seed-time validation in strict mode, transaction rollback, arbitrary SQL rejection, and schema helper utilities.

CLI Integration for Memory Adapter

Layer / File(s) Summary
Memory database detection
packages/cli/src/lib/database.ts
isMemoryDatabase predicate safely checks if a database instance has id === 'memory'.
CLI commands and status
packages/cli/src/commands/generate.ts, packages/cli/src/commands/migrate.ts, packages/cli/src/commands/status.ts, packages/cli/src/lib/status.ts, packages/cli/src/shared/database.ts, packages/cli/src/shared/generators.ts
generate and migrate commands log and return early for memory adapters. status skips SQL count queries for ephemeral databases and reports adapter identity. CliStatus.database extended with adapter and ephemeral fields. DATABASE_ADAPTERS registry and code generators updated to support memory import/initialization.
CLI test
packages/cli/test/cli.test.ts
Verifies getPaymeshStatus reports memory adapter as connected, ephemeral, and skips SQL aggregation.

Documentation and Configuration

Layer / File(s) Summary
Memory adapter documentation
apps/web/content/docs/database/memory.mdx, apps/web/content/docs/database/overview.mdx, apps/web/content/docs/guides/database.mdx, apps/web/content/docs/installation.mdx, apps/web/content/docs/introduction.mdx, packages/paymesh/README.md
New memory adapter page with installation, usage, configuration, and guidance. Overview/guide/introduction/README updates mentioning in-memory as option for tests, CI, and local examples.
Build configuration
lefthook.yml, package.json, .changeset/tidy-bees-jam.md
Pre-commit hook optimized for targeted typechecking based on changed files. Package script reformatted. Release changeset declares version bumps and feature summary.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • usepaymesh/paymesh#31: Both PRs update the CLI database adapter registry in packages/cli/src/shared/database.ts, with the retrieved PR introducing the base postgres/prisma/drizzle set and this PR extending it with the memory adapter.
  • usepaymesh/paymesh#28: Both PRs modify the same CLI status implementation (packages/cli/src/lib/status.ts / getPaymeshStatus), with the retrieved PR handling sandbox scoping and this PR adding memory adapter ephemeral/skip-counts logic.
  • usepaymesh/paymesh#24: Both PRs update CLI command/status code paths (packages/cli/src/commands/generate.ts, packages/cli/src/commands/migrate.ts, packages/cli/src/lib/status.ts), so this PR's memory-specific early-exit logic builds on the retrieved PR's CLI refactors.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning No pull request description was provided by the author, but the template requires brief descriptions of changes, related issues, and a completion checklist. Add a comprehensive description covering: brief overview of the memory adapter feature, confirmation of tests written, changeset file generation, and documentation updates.
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(memory): add new adapter' clearly and specifically describes the main change: introduction of a new memory database adapter.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/memory-adapter

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: 4

🧹 Nitpick comments (2)
packages/memory/src/repositories.ts (1)

222-236: 💤 Low value

Consider removing redundant deleted field.

Line 233 sets deleted: true in addition to setting deletedAt timestamp. The deletedAt field already indicates deletion (non-null means deleted). The boolean deleted field appears redundant unless required by an external API contract.

If deleted is not consumed elsewhere, consider removing it to simplify the data model.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/memory/src/repositories.ts` around lines 222 - 236, The markDeleted
implementation sets both deletedAt and a redundant boolean deleted flag; update
the function markDeleted to stop writing the deleted property (only set
deletedAt to indicate deletion) by removing the deleted: true entry from the
object written to options.getState().customers in
packages/memory/src/repositories.ts, ensure version calculation (getVersion
call) and raw persistence (getPersistableRaw) remain correct, and search for any
downstream consumers expecting customer.deleted and update them to check
deletedAt instead (or reintroduce the boolean only if an external contract
requires it).
packages/memory/src/shared/schema.ts (1)

103-109: ⚖️ Poor tradeoff

Consider documenting mutation risk when cloning fails.

When structuredClone fails and returns the original value, multiple records may share references to the same default value object. Subsequent mutations would affect all records that received that default.

While this is documented behavior, consider whether the caller should be notified (via warning or different return type) when cloning fails, especially for applyTableFieldDefaults where shared mutable defaults could cause subtle bugs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/memory/src/shared/schema.ts` around lines 103 - 109, The cloneValue
function silently returns the original value when structuredClone fails, risking
shared mutable defaults (notably in applyTableFieldDefaults); change
cloneValue<T>(value: T) to accept an optional logger or warn flag and, on catch,
emit a clear warning (via provided logger.warn or console.warn) that
structuredClone failed and the original reference is being returned, including
context (function name, field/table identifier if available) so callers (e.g.,
applyTableFieldDefaults) can surface the risk; update applyTableFieldDefaults to
call cloneValue with the warning/logger enabled so failures are reported rather
than silent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/memory/src/repositories.ts`:
- Around line 135-221: Add missing bidirectional pagination tests for the
customers.list method: create at least 5 customers in the memory repository,
call customers.list with limit=2 and no cursor to get page1, then use the
returned next cursor to fetch page2 and assert that page2 has both previous and
next non-null (middle page), continue to page3 and assert boundary behavior
(previous non-null, next null), then navigate backwards using the before cursor
to page2 and assert both previous and next non-null again; ensure you assert
null/nonnull for previous/next at the correct ends to cover forward and backward
pagination paths through the list implementation.

In `@packages/memory/src/shared/schema.ts`:
- Around line 82-95: The JSDoc and implementation disagree: update
hydrateTableFieldKeys to ensure all schema-defined keys are present by
unconditionally assigning next[field.key] = record[field.key] inside the loop
(so absent keys become undefined and existing null/values are preserved),
keeping the function name hydrateTableFieldKeys and table.fields references
intact; alternatively, if you prefer the current semantics, change the JSDoc to
state that the function only copies schema-defined field values when present.

In `@packages/memory/src/state.ts`:
- Around line 667-672: The product/price seed persistence only reads raw from
product.raw and thus misses provider raw payloads stored under
Symbol.for('paymesh.raw'); update the logic in the block that calls
state.products.set (and the analogous block at lines ~705-710) so that when
persistRaw is true you prefer product.raw if present otherwise extract the
symbol-based payload via product[Symbol.for('paymesh.raw')] (falling back to
null), and store that value as raw; ensure you reference the exact setter
invocation state.products.set and the product object when making this change.
- Around line 640-673: The insertSeedProduct and insertSeedPrice flows currently
skip schema-level required-field validation; update both functions to, when
strict is true, run applyTableFieldDefaults(schema, schema.tables.products) /
applyTableFieldDefaults(schema, schema.tables.prices) on the incoming
product/price object and then call validateRequiredTableFields(schema,
schema.tables.products.name, product) / validateRequiredTableFields(schema,
schema.tables.prices.name, price) before performing validateUniqueInsert and
persisting; ensure the object used for persistence includes the defaulted fields
(and still handle createdAt/updatedAt and raw/persistRaw as before).

---

Nitpick comments:
In `@packages/memory/src/repositories.ts`:
- Around line 222-236: The markDeleted implementation sets both deletedAt and a
redundant boolean deleted flag; update the function markDeleted to stop writing
the deleted property (only set deletedAt to indicate deletion) by removing the
deleted: true entry from the object written to options.getState().customers in
packages/memory/src/repositories.ts, ensure version calculation (getVersion
call) and raw persistence (getPersistableRaw) remain correct, and search for any
downstream consumers expecting customer.deleted and update them to check
deletedAt instead (or reintroduce the boolean only if an external contract
requires it).

In `@packages/memory/src/shared/schema.ts`:
- Around line 103-109: The cloneValue function silently returns the original
value when structuredClone fails, risking shared mutable defaults (notably in
applyTableFieldDefaults); change cloneValue<T>(value: T) to accept an optional
logger or warn flag and, on catch, emit a clear warning (via provided
logger.warn or console.warn) that structuredClone failed and the original
reference is being returned, including context (function name, field/table
identifier if available) so callers (e.g., applyTableFieldDefaults) can surface
the risk; update applyTableFieldDefaults to call cloneValue with the
warning/logger enabled so failures are reported rather than silent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 08153407-c8ce-4a57-ae60-6ea337c5143c

📥 Commits

Reviewing files that changed from the base of the PR and between 7882575 and e85b19b.

📒 Files selected for processing (30)
  • .changeset/tidy-bees-jam.md
  • apps/web/content/docs/database/memory.mdx
  • apps/web/content/docs/database/overview.mdx
  • apps/web/content/docs/guides/database.mdx
  • apps/web/content/docs/installation.mdx
  • apps/web/content/docs/introduction.mdx
  • lefthook.yml
  • package.json
  • packages/cli/src/commands/generate.ts
  • packages/cli/src/commands/migrate.ts
  • packages/cli/src/commands/status.ts
  • packages/cli/src/lib/database.ts
  • packages/cli/src/lib/status.ts
  • packages/cli/src/shared/database.ts
  • packages/cli/src/shared/generators.ts
  • packages/cli/test/cli.test.ts
  • packages/memory/README.md
  • packages/memory/package.json
  • packages/memory/src/index.ts
  • packages/memory/src/repositories.ts
  • packages/memory/src/shared/cursor.ts
  • packages/memory/src/shared/driver.ts
  • packages/memory/src/shared/raw.ts
  • packages/memory/src/shared/schema.ts
  • packages/memory/src/state.ts
  • packages/memory/src/types.ts
  • packages/memory/test/memory.test.ts
  • packages/memory/tsconfig.json
  • packages/memory/tsdown.config.mjs
  • packages/paymesh/README.md

Comment on lines +135 to +221
async list(_schema, provider, sandbox, listOptions) {
const limit = listOptions?.limit ?? 20;
if (!Number.isInteger(limit) || limit <= 0) {
throw new PaymeshError({
code: 'invalid_request',
message: 'Customer list limit must be a positive integer',
});
}
if (listOptions?.after && listOptions?.before) {
throw new PaymeshError({
code: 'invalid_request',
message:
'Customer list accepts either "after" or "before", not both',
});
}

const cursor = decodeCustomerCursor(
listOptions?.before ?? listOptions?.after,
listOptions?.before ? 'before' : 'after',
);
const filtered = [...options.getState().customers.values()]
.filter(
(customer) =>
customer.provider === provider &&
customer.sandbox === sandbox &&
customer.deletedAt === null,
)
.sort(compareCustomerRows);

let pageSource = filtered;
if (cursor?.mode === 'after') {
pageSource = filtered.filter(
(customer) =>
compareCustomerRowToCursor(customer, cursor.value) > 0,
);
} else if (cursor?.mode === 'before') {
pageSource = filtered
.filter(
(customer) =>
compareCustomerRowToCursor(customer, cursor.value) < 0,
)
.sort((left, right) => compareCustomerRows(right, left));
}

const hasExtra = pageSource.length > limit;
const windowRows = hasExtra ? pageSource.slice(0, limit) : pageSource;
const pageRows =
cursor?.mode === 'before' ? [...windowRows].reverse() : windowRows;
const data = pageRows.map((customer) =>
mapStoredCustomer(customer, listOptions),
);

return {
data,
total: filtered.length,
previous:
data.length === 0
? null
: cursor?.mode === 'before'
? hasExtra
? encodeCustomerCursor({
createdAt: pageRows[0]!.createdAt,
providerId: pageRows[0]!.id,
})
: null
: cursor
? encodeCustomerCursor({
createdAt: pageRows[0]!.createdAt,
providerId: pageRows[0]!.id,
})
: null,
next:
data.length === 0
? null
: cursor?.mode === 'before'
? encodeCustomerCursor({
createdAt: pageRows[pageRows.length - 1]!.createdAt,
providerId: pageRows[pageRows.length - 1]!.id,
})
: hasExtra
? encodeCustomerCursor({
createdAt: pageRows[pageRows.length - 1]!.createdAt,
providerId: pageRows[pageRows.length - 1]!.id,
})
: null,
};
},

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for customer list pagination test cases
rg -n -C3 'customers\.list' --glob '*test*'

Repository: usepaymesh/paymesh

Length of output: 4890


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the pagination tests we already found
sed -n '200,320p' packages/paymesh/test/database.test.ts
sed -n '480,620p' packages/paymesh/test/client.test.ts
sed -n '1,200p' packages/memory/test/memory.test.ts

Repository: usepaymesh/paymesh

Length of output: 10621


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n 'customers\.list\(\{[^}]*before:' --glob '*test*' .
rg -n 'customers\.list\(\{[^}]*after:' --glob '*test*' .
rg -n '\.previous' --glob '*test*' packages/paymesh/test packages/memory/test
rg -n '\.next' --glob '*test*' packages/paymesh/test packages/memory/test

Repository: usepaymesh/paymesh

Length of output: 44


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n 'repositories\.customers\.list' --glob '*test*' packages
rg -n 'customers\.list\(' --glob '*test*' packages/paymesh/test packages/memory/test
rg -n '\b(before|after)\b' --glob '*test*' packages/paymesh/test packages/memory/test
rg -n '\.previous\b|\.next\b' --glob '*test*' packages/paymesh/test packages/memory/test

Repository: usepaymesh/paymesh

Length of output: 4230


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n 'repositories\.customers\.list' --glob '*test*' packages
rg -n 'customers\.list\(' --glob '*test*' packages/paymesh/test packages/memory/test
rg -n '\b(before|after)\b' --glob '*test*' packages/paymesh/test packages/memory/test
rg -n '\.previous\b|\.next\b' --glob '*test*' packages/paymesh/test packages/memory/test

Repository: usepaymesh/paymesh

Length of output: 4230


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n '\bbefore\s*:' packages/paymesh/test/database.test.ts
rg -n '\bafter\s*:' packages/paymesh/test/database.test.ts
rg -n 'customers\.list\(' packages/paymesh/test/database.test.ts

# Show the cursor-related test we already saw and a bit more context
sed -n '200,320p' packages/paymesh/test/database.test.ts

# If there are other pagination tests using the same list logic, inspect nearby cursor stubs/fixtures
sed -n '1080,1210p' packages/paymesh/test/database.test.ts

Repository: usepaymesh/paymesh

Length of output: 7055


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the exact implementation under review
sed -n '110,260p' packages/memory/src/repositories.ts

# Also check the cursor comparison helpers used by list() pagination
rg -n 'decodeCustomerCursor|encodeCustomerCursor|compareCustomerRowToCursor|compareCustomerRows' packages/memory/src/repositories.ts

Repository: usepaymesh/paymesh

Length of output: 5371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect cursor helpers
sed -n '1,220p' packages/memory/src/shared/cursor.ts

# Inspect ordering + cursor comparisons
sed -n '520,700p' packages/memory/src/repositories.ts

Repository: usepaymesh/paymesh

Length of output: 4141


Extend customers.list bidirectional cursor tests (before/after).

packages/memory/src/repositories.ts computes previous/next differently for after vs before, and while existing tests cover the “first → second (after)” and “second → first (before)” transitions, they don’t cover middle-page scenarios where both cursors should be present (and the opposite end should be null). Add tests that paginate through at least 3+ pages (e.g., 5 customers with limit=2) to cover:

  • forward pagination to a middle page (both previous and next non-null)
  • backward pagination from a later page to a middle page (both previous and next non-null)
  • boundary pages for before mode to ensure previous/next are null at the correct ends.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/memory/src/repositories.ts` around lines 135 - 221, Add missing
bidirectional pagination tests for the customers.list method: create at least 5
customers in the memory repository, call customers.list with limit=2 and no
cursor to get page1, then use the returned next cursor to fetch page2 and assert
that page2 has both previous and next non-null (middle page), continue to page3
and assert boundary behavior (previous non-null, next null), then navigate
backwards using the before cursor to page2 and assert both previous and next
non-null again; ensure you assert null/nonnull for previous/next at the correct
ends to cover forward and backward pagination paths through the list
implementation.

Comment on lines +82 to +95
export function hydrateTableFieldKeys<TRecord extends Record<string, unknown>>(
record: TRecord,
table: ResolvedDatabaseTable,
) {
const next: Record<string, unknown> = { ...record };

for (const field of Object.values(table.fields)) {
if (record[field.key] !== undefined && record[field.key] !== null) {
next[field.key] = record[field.key];
}
}

return next;
}

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 | ⚡ Quick win

Comment does not match implementation behavior.

The JSDoc comment on line 79 states "Ensures that all schema-defined keys are present in the returned object", but the implementation only copies keys that exist and are non-null/non-undefined in the input record. Keys that are absent or null in the input will not be present in the returned object.

If the intent is to ensure all schema keys are present (e.g., initialized to undefined), the loop should unconditionally set next[field.key]. If the current behavior is correct, update the comment to reflect that it "copies schema-defined field values when present."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/memory/src/shared/schema.ts` around lines 82 - 95, The JSDoc and
implementation disagree: update hydrateTableFieldKeys to ensure all
schema-defined keys are present by unconditionally assigning next[field.key] =
record[field.key] inside the loop (so absent keys become undefined and existing
null/values are preserved), keeping the function name hydrateTableFieldKeys and
table.fields references intact; alternatively, if you prefer the current
semantics, change the JSDoc to state that the function only copies
schema-defined field values when present.

Comment on lines +640 to +673
function insertSeedProduct(
state: MemoryDatabaseState,
schema: ResolvedDatabaseSchema,
product: MemorySeedProduct,
strict: boolean,
persistRaw: boolean,
) {
validateRequiredString(product.id, 'id', schema.tables.products.name);
validateRequiredString(
product.provider,
'provider',
schema.tables.products.name,
);
validateRequiredBoolean(
product.sandbox,
'sandbox',
schema.tables.products.name,
);

const key = entityKey(product.provider, product.sandbox, product.id);
validateUniqueInsert(
strict,
state.products.has(key),
schema.tables.products.name,
product.id,
);
const now = product.createdAt ?? new Date().toISOString();
state.products.set(key, {
...product,
createdAt: now,
updatedAt: product.updatedAt ?? now,
raw: persistRaw ? (product.raw ?? null) : null,
});
}

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 | ⚡ Quick win

Strict-mode seed validation is incomplete for products/prices.

insertSeedProduct and insertSeedPrice skip schema-level required-field validation (applyTableFieldDefaults + validateRequiredTableFields) that other seeded entities use. In strict mode, this allows invalid catalog rows to be inserted and fail later in downstream flows.

Suggested fix
 function insertSeedProduct(
   state: MemoryDatabaseState,
   schema: ResolvedDatabaseSchema,
   product: MemorySeedProduct,
   strict: boolean,
   persistRaw: boolean,
 ) {
+  const next = applyTableFieldDefaults(
+    schema.tables.products,
+    product as unknown as Record<string, unknown>,
+  );
+  validateRequiredTableFields(schema, 'products', next);
+  const productRecord = next as Record<string, unknown>;
-  validateRequiredString(product.id, 'id', schema.tables.products.name);
+  validateRequiredString(productRecord.id, 'id', schema.tables.products.name);
   ...
 }
 
 function insertSeedPrice(
   state: MemoryDatabaseState,
   schema: ResolvedDatabaseSchema,
   price: MemorySeedPrice,
   strict: boolean,
   persistRaw: boolean,
 ) {
+  const next = applyTableFieldDefaults(
+    schema.tables.prices,
+    price as unknown as Record<string, unknown>,
+  );
+  validateRequiredTableFields(schema, 'prices', next);
+  const priceRecord = next as Record<string, unknown>;
-  validateRequiredString(price.id, 'id', schema.tables.prices.name);
+  validateRequiredString(priceRecord.id, 'id', schema.tables.prices.name);
   ...
 }

Also applies to: 675-711

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/memory/src/state.ts` around lines 640 - 673, The insertSeedProduct
and insertSeedPrice flows currently skip schema-level required-field validation;
update both functions to, when strict is true, run
applyTableFieldDefaults(schema, schema.tables.products) /
applyTableFieldDefaults(schema, schema.tables.prices) on the incoming
product/price object and then call validateRequiredTableFields(schema,
schema.tables.products.name, product) / validateRequiredTableFields(schema,
schema.tables.prices.name, price) before performing validateUniqueInsert and
persisting; ensure the object used for persistence includes the defaulted fields
(and still handle createdAt/updatedAt and raw/persistRaw as before).

Comment on lines +667 to +672
state.products.set(key, {
...product,
createdAt: now,
updatedAt: product.updatedAt ?? now,
raw: persistRaw ? (product.raw ?? null) : null,
});

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 | ⚡ Quick win

Raw payload persistence is inconsistent for catalog seeds.

For product/price seeds, raw is read only from record.raw, while other entities also extract hidden raw payloads via Symbol.for('paymesh.raw'). With persistRaw: true, this drops provider raw payloads for catalog records when they are attached via symbol-based metadata.

Suggested fix
-    raw: persistRaw ? (product.raw ?? null) : null,
+    raw: persistRaw
+      ? ((product as Record<string, unknown>).raw ?? getInternalRaw(product) ?? null)
+      : null,
...
-    raw: persistRaw ? (price.raw ?? null) : null,
+    raw: persistRaw
+      ? ((price as Record<string, unknown>).raw ?? getInternalRaw(price) ?? null)
+      : null,

Also applies to: 705-710

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/memory/src/state.ts` around lines 667 - 672, The product/price seed
persistence only reads raw from product.raw and thus misses provider raw
payloads stored under Symbol.for('paymesh.raw'); update the logic in the block
that calls state.products.set (and the analogous block at lines ~705-710) so
that when persistRaw is true you prefer product.raw if present otherwise extract
the symbol-based payload via product[Symbol.for('paymesh.raw')] (falling back to
null), and store that value as raw; ensure you reference the exact setter
invocation state.products.set and the product object when making this change.

@almeidazs
almeidazs merged commit 61fc3e5 into main Jun 11, 2026
13 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(memory): add new database adapter that uses the memory

1 participant