Skip to content

Conversation

bryanjnelson
Copy link
Contributor

@bryanjnelson bryanjnelson commented Nov 12, 2024

Partially addresses #3454.

Where this stands:

  • the refactoring of the code related to Neo4j and the rest of the product functionality is ready for review
    - the Gel queries are blocked due to an issue where the TS server crashes on account of the types being created
    - this issue happened before and Carson was able to modify the types to eliminate the complexity causing it
    - after that is resolved then further work can be done to complete the Gel queries
  • Gel queries currently in progress

Copy link

coderabbitai bot commented Nov 12, 2024

📝 Walkthrough

Walkthrough

Refactors product types and repository/service flows, introduces an EdgeDB-backed repository, updates DTO field types, adjusts a migration to new APIs/data shapes, tweaks resolver access for nested IDs, and consolidates create/read/update/delete paths to return secured DTOs via repository-based methods.

Changes

Cohort / File(s) Summary
DTO type updates and exports
src/components/product/dto/product.dto.ts
Re-typed Product.engagement and Product.project from ID to LinkTo<'LanguageEngagement'> and LinkTo<'Project'>; added type-only import for LinkTo; exported ProductConcretes aggregating concrete product classes.
Migration API and data-shape adjustments
src/components/product/migrations/fix-nan-total-verse-equivalents.migration.ts
Switched product fetch to productService.readMany(ids); passed scriptureReferences.value to getTotalVerseEquivalents.
New EdgeDB repository layer
src/components/product/product.edgedb.repository.ts
Added EdgeDB-backed ProductEdgedbRepository with hydrate shapes, concrete repositories map, lazy concretes getter, createDirect/Derivative/Other wrappers, and list methods for IDs with scripture refs and PnP indexes.
Repository refactor and DTO mapping
src/components/product/product.repository.ts
Introduced readOne/Unsecured variants and readManyUnsecured; added DTO mapper; changed create methods to return DTOs; added specialized update methods (direct/derivative/other); added delete by DTO; adjusted hydration to nested engagement/project objects; integrated ScriptureReferenceService and ResourceResolver.
Resolver nested ID access
src/components/product/product.resolver.ts
Updated project field resolver to use product.project.id when loading non-OnlyId path; OnlyId path unchanged.
Service flow overhaul
src/components/product/product.service.ts
Shifted to repository-centric create/read/update/delete; injected EngagementService; removed unsecured read helpers and internal DTO mapping; create paths use repo.create* returning secured DTOs; updates use repo.update*; delete reads unsecured DTO then repo.delete; list secures repo.list results.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Server errors for creation failures #3392 — Also refactors product creation and post-creation read handling in product.service.ts and product.repository.ts, touching the same areas and patterns as this PR.

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 0938-edgedb-product-queries

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@bryanjnelson bryanjnelson changed the title [EdgeDB] Add Product queries | refactor service/repo layers appropriately [EdgeDB] Add Product queries | refactor service/repo layers appropriately Nov 15, 2024
@bryanjnelson bryanjnelson force-pushed the 0938-edgedb-product-queries branch 3 times, most recently from 5aa5778 to 1b1a3fc Compare December 6, 2024 20:28
@bryanjnelson bryanjnelson changed the title [EdgeDB] Add Product queries | refactor service/repo layers appropriately [Gel] Add Product queries | refactor service/repo layers appropriately May 30, 2025
@bryanjnelson bryanjnelson changed the title [Gel] Add Product queries | refactor service/repo layers appropriately [Gel] Add Product queries | refactor service/repo layers May 30, 2025
@bryanjnelson bryanjnelson force-pushed the 0938-edgedb-product-queries branch 2 times, most recently from 9f9fe90 to b7567c7 Compare August 26, 2025 21:21
@bryanjnelson bryanjnelson marked this pull request as ready for review August 28, 2025 17:25
@bryanjnelson bryanjnelson requested a review from CarsonF as a code owner August 28, 2025 17:25
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

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

⚠️ Outside diff range comments (1)
src/components/product/product.service.ts (1)

88-105: Derivative totals computed as 0 when no override.
If scriptureReferencesOverride is undefined, totals should fall back to the producible’s scripture references.

-      totalVerses = getTotalVerses(
-        ...(derivativeInput.scriptureReferencesOverride ?? []),
-      );
-      totalVerseEquivalents = getTotalVerseEquivalents(
-        ...(derivativeInput.scriptureReferencesOverride ?? []),
-      );
+      const effectiveScripture =
+        derivativeInput.scriptureReferencesOverride ??
+        (await this.repo.getProducibleScriptureReferences(
+          derivativeInput.produces,
+        ));
+      totalVerses = getTotalVerses(...effectiveScripture);
+      totalVerseEquivalents = getTotalVerseEquivalents(...effectiveScripture);

If getProducibleScriptureReferences() doesn’t exist, I can add it to both repos.

🧹 Nitpick comments (6)
src/components/product/dto/product.dto.ts (1)

269-274: Exporting ProductConcretes: add as const for stronger typing.
Prevents key widening and helps downstream keyof usages.

-export const ProductConcretes = {
+export const ProductConcretes = {
   DirectScriptureProduct,
   DerivativeScriptureProduct,
   OtherProduct,
-};
+} as const;
src/components/product/product.edgedb.repository.ts (1)

181-202: Return types are any; tighten them for safety.
Helps callers and prevents accidental shape drift.

-import { type ID, type PublicOf } from '../../common';
+import { type ID, type PublicOf, type UnsecuredDto } from '../../common';
@@
-  Product,
+  Product,
+  DirectScriptureProduct,
+  DerivativeScriptureProduct,
+  OtherProduct,
 } from './dto';
@@
-  async createDerivative(
+  async createDerivative(
     input: CreateDerivativeScriptureProduct & {
       totalVerses: number;
       totalVerseEquivalents: number;
     },
-  ) {
+  ): Promise<UnsecuredDto<DerivativeScriptureProduct>> {
     return await this.concretes.DerivativeScriptureProduct.create(input);
   }
@@
-  async createDirect(
+  async createDirect(
     input: CreateDirectScriptureProduct & {
       totalVerses: number;
       totalVerseEquivalents: number;
     },
-  ) {
+  ): Promise<UnsecuredDto<DirectScriptureProduct>> {
     return await this.concretes.DirectScriptureProduct.create(input);
   }
@@
-  async createOther(input: CreateOtherProduct) {
+  async createOther(input: CreateOtherProduct): Promise<UnsecuredDto<OtherProduct>> {
     return await this.concretes.OtherProduct.create(input);
   }
src/components/product/product.service.ts (2)

122-147: Create permission is verified after write.
This can write unauthorized records if repo doesn’t enforce auth. Prefer verifying before persisting.

-    const created = otherInput
+    // Verify create permission before persisting; use a synthetic DTO if needed.
+    // const draftDto = this.buildDraftDto(input, steps, progressTarget, totalVerses, totalVerseEquivalents);
+    // this.privileges.for(resolveProductType(draftDto), draftDto).verifyCan('create');
+
+    const created = otherInput
       ? await this.repo.createOther({ ...otherInput, progressTarget, steps })
       : 'produces' in input
       ? await this.repo.createDerivative({
           ...input,
           progressTarget,
           steps,
           totalVerses,
           totalVerseEquivalents,
         })
       : await this.repo.createDirect({
           ...input,
           progressTarget,
           steps,
           totalVerses,
           totalVerseEquivalents,
         });
 
     const securedCreated = this.secure(created);
 
-    this.privileges
+    this.privileges
       .for(resolveProductType(securedCreated), securedCreated)
       .verifyCan('create');
 
     return securedCreated;

173-191: UpdateDirect flow looks sound; keep TODO in mind.
Consider moving description-merge into repo later.

src/components/product/product.repository.ts (2)

99-116: Clarify return types for readOne/readOneUnsecured.

readOne returns a hydrated DB row (dto), while readOneUnsecured returns an UnsecuredDto. Add explicit return types to prevent misuse at call sites.

Apply:

-  async readOne(id: ID) {
+  async readOne(id: ID): Promise<HydratedProductRow> {

-  async readOneUnsecured(id: ID) {
+  async readOneUnsecured(id: ID): Promise<UnsecuredDto<AnyProduct>> {

331-350: Annotate creation method return types for stronger guarantees.

These methods return Unsecured DTOs; annotate them (including the private create path).

Apply:

-  async createDerivative(
+  async createDerivative(
     input: CreateDerivativeScriptureProduct & {
       totalVerses: number;
       totalVerseEquivalents: number;
     },
-  ) {
+  ): Promise<UnsecuredDto<DerivativeScriptureProduct>> {
     return (await this.create(
       input,
     )) as UnsecuredDto<DerivativeScriptureProduct>;
   }

-  async createDirect(
+  async createDirect(
     input: CreateDirectScriptureProduct & {
       totalVerses: number;
       totalVerseEquivalents: number;
     },
-  ) {
+  ): Promise<UnsecuredDto<DirectScriptureProduct>> {
     return (await this.create(input)) as UnsecuredDto<DirectScriptureProduct>;
   }

-  private async create(
+  private async create(
     input: (CreateDerivativeScriptureProduct | CreateDirectScriptureProduct) & {
       totalVerses: number;
       totalVerseEquivalents: number;
     },
-  ) {
+  ): Promise<UnsecuredDto<AnyProduct>> {

Also applies to: 351-356

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b7daeb8 and b7567c7.

📒 Files selected for processing (6)
  • src/components/product/dto/product.dto.ts (3 hunks)
  • src/components/product/migrations/fix-nan-total-verse-equivalents.migration.ts (1 hunks)
  • src/components/product/product.edgedb.repository.ts (1 hunks)
  • src/components/product/product.repository.ts (14 hunks)
  • src/components/product/product.resolver.ts (1 hunks)
  • src/components/product/product.service.ts (16 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/components/product/dto/product.dto.ts (1)
src/core/resources/resource.loader.ts (1)
  • LinkTo (34-39)
src/components/product/product.edgedb.repository.ts (3)
src/common/types.ts (1)
  • PublicOf (28-28)
src/common/instance-maps.ts (1)
  • grabInstances (10-15)
src/common/id-field.ts (1)
  • ID (24-25)
src/components/product/migrations/fix-nan-total-verse-equivalents.migration.ts (1)
src/components/scripture/verse-equivalents.ts (1)
  • getTotalVerseEquivalents (12-22)
src/components/product/product.repository.ts (6)
src/components/scripture/scripture-reference.service.ts (1)
  • ScriptureReferenceService (10-69)
src/common/secured-property.ts (1)
  • UnsecuredDto (57-59)
src/components/product/dto/product.dto.ts (1)
  • AnyProduct (229-232)
src/core/database/results/parse-base-node.ts (1)
  • BaseNode (5-12)
src/common/exceptions/exception.ts (1)
  • ServerException (14-14)
src/components/product/dto/producible.dto.ts (2)
  • ProducibleType (49-49)
  • ProducibleType (50-55)
🔇 Additional comments (24)
src/components/product/dto/product.dto.ts (2)

21-21: Good: type-only import for LinkTo avoids runtime side effects.


58-60: Switch to LinkTo shape is fine; ensure resolvers use .id.
Project resolver’s OnlyId branch still returns { id: product.project }. It must use product.project.id. See resolver comment with fix.

src/components/product/product.edgedb.repository.ts (4)

18-41: Potential invalid .slice(9, null) usage in GEL expression.
Confirm that slice(9, null) is valid; most APIs treat undefined/omission as “to end”.

-    labels: e.array_agg(e.set(product.engagement.__type__.name.slice(9, null))),
+    labels: e.array_agg(e.set(product.engagement.__type__.name.slice(9))),

77-91: Verify produces.scriptureReferences shape matches GraphQL DTO.
e.tuple([dsp.produces.scripture]) might not equal the expected ScriptureRange[]. Consider returning the set directly or array_agg.

-    produces: {
-      scriptureReferences: e.tuple([dsp.produces.scripture]),
+    produces: {
+      // If multiple ranges, aggregate to array; adjust to match SecuredProducible mapping
+      scriptureReferences: e.array_agg(dsp.produces.scripture),
       createdAt: dsp.produces.createdAt,
       id: dsp.produces.id,
     },

106-164: Concrete repo providers: ensure they’re registered in the module.
These classes must be included in providers so ModuleRef.get() can resolve them.


165-172: Repo base looks good.
Hydrate/omit config aligns with DTOs.

src/components/product/migrations/fix-nan-total-verse-equivalents.migration.ts (2)

30-32: Good: pass inner .value to VEs calculation.
Matches expected API of getTotalVerseEquivalents.


27-27: Prefer direct repository reads in migrations: secured service reads (e.g. productService.readMany) can omit scriptureReferences.value. Use the repository’s unsecured method to load all fields:

-    const products = await this.productService.readMany(ids);
+    // Unsecured read to include scriptureReferences
+    const products = await this.productService['repo'].readManyUnsecured(ids);

Or inject the repository into the migration instead of relying on the secured DTO.

src/components/product/product.service.ts (6)

62-63: Good: validate engagement existence up front.
Avoids orphaned products.


155-162: Good: readOne now returns secured DTOs.
Aligns service API and callers.


251-259: UpdateDerivative mirrors direct; OK.


311-340: UpdateOther: reads unsecured, verifies, merges description, then updates — OK.


423-428: Delete path: read unsecured then enforce privilege — OK.


432-437: List path: secure items after repo list — OK.

src/components/product/product.repository.ts (10)

12-12: Imports and type wiring look consistent.

New dependencies and DTO types align with usages below.

Also applies to: 17-21, 23-23, 39-44, 52-52, 68-68, 70-70


91-96: Confirm DI provider registration and avoid circulars.

Ensure ScriptureReferenceRepository, ScriptureReferenceService, and ResourceResolver are provided in the module and don’t introduce circular deps. If any are optional, mark accordingly.


127-133: LGTM: Unsecured mapper over readMany.

Mapping hydrated rows to UnsecuredDto keeps the API consistent with the new DTO flow.


276-278: Breaking shape change: engagement/project now nested objects.

Downstream DTOs/resolvers expecting raw IDs must be updated to handle { id }. Verify GraphQL mappers and any serialization that previously used flat IDs.


288-305: LGTM: Direct updates split scripture + unspecifiedScripture from simple props.

Separation minimizes unintended property writes and uses ScriptureReferenceService correctly.


443-444: LGTM: Create flows return mapped DTOs.

Returning mapped DTOs immediately after creation simplifies caller logic and keeps shapes uniform.

Also applies to: 480-483


577-587: LGTM: Other updates return mapped DTO.

Consistent with other update paths; minimal surface area for callers.


619-623: LGTM: Paginated list maps items to DTOs.

Keeps pagination metadata intact while normalizing item shape.


648-657: Good error wrapping on delete.

ServerException with cause retains context while hiding low-level details.


724-747: LGTM: Derivative mapping with override semantics.

Resolve producible type from labels and switch scripture refs based on isOverriding—clean and readable.

Comment on lines 216 to 228
async listIdsWithPnpIndexes(engagementId: ID, _type?: string) {
const engagement = e.cast(e.LanguageEngagement, e.uuid(engagementId));

const query = e.select(e.Product, (p) => ({
id: true,
pnpIndex: p.pnpIndex,
...e.is(e.DirectScriptureProduct, {}),
filter: e.op(p.engagement, '=', engagement),
}));

return await this.db.run(query);
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Type filter is ignored in listIdsWithPnpIndexes.
Implement the filter or drop the param.

-  async listIdsWithPnpIndexes(engagementId: ID, _type?: string) {
+  async listIdsWithPnpIndexes(engagementId: ID, _type?: string) {
     const engagement = e.cast(e.LanguageEngagement, e.uuid(engagementId));
-
-    const query = e.select(e.Product, (p) => ({
-      id: true,
-      pnpIndex: p.pnpIndex,
-      ...e.is(e.DirectScriptureProduct, {}),
-      filter: e.op(p.engagement, '=', engagement),
-    }));
+    const query =
+      _type === 'DirectScriptureProduct'
+        ? e.select(e.DirectScriptureProduct, (p) => ({
+            id: true,
+            pnpIndex: p.pnpIndex,
+            filter: e.op(p.engagement, '=', engagement),
+          }))
+        : e.select(e.Product, (p) => ({
+            id: true,
+            pnpIndex: p.pnpIndex,
+            filter: e.op(p.engagement, '=', engagement),
+          }));
 
     return await this.db.run(query);
   }
📝 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
async listIdsWithPnpIndexes(engagementId: ID, _type?: string) {
const engagement = e.cast(e.LanguageEngagement, e.uuid(engagementId));
const query = e.select(e.Product, (p) => ({
id: true,
pnpIndex: p.pnpIndex,
...e.is(e.DirectScriptureProduct, {}),
filter: e.op(p.engagement, '=', engagement),
}));
return await this.db.run(query);
}
}
async listIdsWithPnpIndexes(engagementId: ID, _type?: string) {
const engagement = e.cast(e.LanguageEngagement, e.uuid(engagementId));
const query =
_type === 'DirectScriptureProduct'
? e.select(e.DirectScriptureProduct, (p) => ({
id: true,
pnpIndex: p.pnpIndex,
filter: e.op(p.engagement, '=', engagement),
}))
: e.select(e.Product, (p) => ({
id: true,
pnpIndex: p.pnpIndex,
filter: e.op(p.engagement, '=', engagement),
}));
return await this.db.run(query);
}
}

@bryanjnelson bryanjnelson marked this pull request as draft August 28, 2025 17:49
@bryanjnelson bryanjnelson force-pushed the 0938-edgedb-product-queries branch from b7567c7 to c107d85 Compare August 28, 2025 17:58
@bryanjnelson bryanjnelson force-pushed the 0938-edgedb-product-queries branch from c107d85 to a0457a4 Compare August 29, 2025 15:22
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.

2 participants