Skip to content

feat(orm): add Model.Copy and field copy metadata#187

Merged
buke merged 7 commits into
mainfrom
feat/orm-model-copy
Jul 25, 2026
Merged

feat(orm): add Model.Copy and field copy metadata#187
buke merged 7 commits into
mainfrom
feat/orm-model-copy

Conversation

@buke

@buke buke commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

User description

Summary

  • Add Model.Copy(id, defaults?) and instance copy that Browse → buildCopyValues → Create (PR-P0-M2).
  • Introduce @Field({ copy }) metadata (default true); BaseModel Id / CreatedAt / UpdatedAt / DeletedAt are copy: false.
  • Relation strategy B: OneToMany shallow-copy via { create: [...] }; ManyToMany relink via id arrays (no target Copy); default soft-delete visibility.
  • Pipe copy through FieldsGet and Go IrField / backendtsparser / webapistore.

Test plan

  • ./choysum test typecheck core
  • ./choysum test unit core --be --pattern 'Copy|copy'
  • ./choysum test unit core --be (1830 ok)
  • Go: backendtsparser CopyFalse + webapistore CopyContract
  • CI green on this PR

Made with Cursor


PR Type

Enhancement


Description

  • Go Core: Add copy flag to field AST and webapistore generator.

  • TS ORM: Support @field({ copy }) decorator option and metadata.

  • TS ORM: Add Model.Copy and instance.copy methods for record duplication.

  • TS ORM: Support OneToMany shallow copying and ManyToMany relinking.


File Walkthrough

Relevant files
Enhancement
9 files
webapistore.go
Include Copy metadata in web API store field metadata       
+5/-0     
webapistore.ts.tpl
Add copy metadata field to web API store template               
+1/-0     
field_resolved.go
Parse copy option from field decorator into structural spec
+3/-0     
meta_field.go
Add Copy property to IrFieldStructuralSpec AST                     
+2/-0     
field.ts
Validate and record copy flag in Field decorator                 
+8/-0     
field.ts
Add copy option to field metadata type definitions             
+10/-0   
model.ts
Mark system fields copy false and expose Copy methods       
+28/-1   
model_copy.ts
Implement record copy logic, selection building, and relations
+252/-0 
model_fields_get_facade.ts
Expose copy metadata in FieldsGet response payload             
+5/-0     
Tests
4 files
webapistore_test.go
Add unit test for web API store copy metadata conversion 
+34/-0   
parser_migration_test.go
Add parser unit test for copy option on fields                     
+45/-0   
field.test.ts
Add unit tests for decorator copy option handling               
+18/-0   
model_copy.test.ts
Add comprehensive unit tests for Model.Copy functionality
+271/-0 

Summary by CodeRabbit

  • New Features
    • Added Model.Copy and instance .copy() to duplicate records by ID, honoring field-level copy rules (including nested relations) and merging defaults.
    • Introduced a copy option on @Field (validated); system Id and timestamp fields are marked non-copyable.
    • Web API store field metadata now conditionally includes copy: false when explicitly configured.
  • Bug Fixes
    • copy: false is parsed, propagated, and emitted consistently across contracts, runtime ORM metadata, and generated store templates (omitted when not set / when copy: true).
  • Tests
    • Added parsing, template rendering, and ORM copy coverage for copy: false and omission behavior.

- Add Model.Copy / instance.copy that Browse, build copy values, then Create.

- Introduce @field({ copy }) with BaseModel system fields marked copy: false.

- Relink ManyToMany by id arrays and shallow-copy OneToMany via create commands.

- Pipe copy through FieldsGet and Go IrField / parser / webapistore.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds a copy field option, propagates explicit exclusions through parser and generated metadata, and introduces static and instance model-copy APIs. Copy operations browse source records, transform scalar and relational values, and create duplicates with optional defaults.

Changes

Model Copy Support

Layer / File(s) Summary
Copy metadata contract and propagation
modules/core/service/orm/metadata/field.ts, pkg/meta/meta_field.go, internal/parser/backendtsparser/..., internal/module/artifact/generate/...
Adds optional copy metadata, parses copy: false, and emits the flag through generated Web API field metadata.
Decorator metadata and system fields
modules/core/service/orm/decorator/..., modules/core/service/orm/model/model_fields_get_facade.ts, modules/core/service/orm/model/model.ts
Validates and stores the decorator option, exposes explicit exclusions through FieldsGet, and marks identifiers and timestamps as non-copyable.
Copy engine and model APIs
modules/core/service/orm/model/model_copy.ts, modules/core/service/orm/model/model.ts
Adds Browse-to-Create copying with scalar, OneToMany, ManyToMany, and ManyToOne handling, depth and cycle checks, defaults, and static/instance entry points.
Copy behavior validation
modules/core/service/orm/model/model_copy.test.ts, modules/core/service/orm/decorator/field.test.ts, internal/parser/backendtsparser/parser_migration_test.go, internal/module/artifact/generate/webapistore_test.go
Tests copy metadata propagation, validation, field filtering, relation conversion, orchestration, errors, and missing relations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BaseModel
  participant copyModel
  participant Browse
  participant buildCopyValues
  participant createModel
  BaseModel->>copyModel: provide source id and defaults
  copyModel->>Browse: browse source with copy selection
  Browse-->>copyModel: return source row
  copyModel->>buildCopyValues: build create payload
  buildCopyValues-->>copyModel: return transformed values
  copyModel->>createModel: persist duplicate
  createModel-->>BaseModel: return created model
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the change, but it omits the required CLA template block from the repository template. Add the CLA agreement text from the template so the PR description matches the repository’s required structure.
Docstring Coverage ⚠️ Warning Docstring coverage is 29.41% 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 is concise and accurately summarizes the main change: Model.Copy plus copy metadata support.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/orm-model-copy

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

@github-actions

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Incorrect Template Condition

In webapistore.ts.tpl, {{- if $field.Copy}}copy: {{$field.Copy}},{{- end}} checks if $field.Copy. Because Go's text/template dereferences *bool pointers during if evaluation, metadata.Copy pointing to false (which is set when @Field({ copy: false })) evaluates to false. Consequently, copy: false is omitted from the generated webapistore TS metadata output instead of rendering copy: false.

Using {{- if ne $field.Copy nil}}copy: {{$field.Copy}},{{- end}} ensures copy: false is emitted when metadata.Copy is explicitly set to false.

{{- if $field.Copy}}copy: {{$field.Copy}},{{- end}}

Comment thread modules/core/service/orm/model/model_copy.ts Fixed
Comment thread modules/core/service/orm/model/model_copy.ts Fixed
@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.30556% with 1 line in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
modules/base/service/models/sequence.ts 0.0% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
modules/core/service/orm/model/model_copy.ts (1)

24-26: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unused isRelationType helper.

Not called anywhere in this file, and it isn't exported. buildCopyBrowseSelection/buildCopyValues re-check field.type literals directly instead.

🤖 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 `@modules/core/service/orm/model/model_copy.ts` around lines 24 - 26, Remove
the unused, non-exported isRelationType helper from this file. Leave
buildCopyBrowseSelection and buildCopyValues unchanged, including their existing
direct field.type checks.
modules/core/service/orm/model/model.ts (1)

457-467: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unnecessary type cast for this.Id.

Id is already declared as public readonly Id: string on this class; the cast to { Id?: string } is redundant.

♻️ Simplify
-    const id = String((this as { Id?: string }).Id || '').trim();
+    const id = String(this.Id || '').trim();
🤖 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 `@modules/core/service/orm/model/model.ts` around lines 457 - 467, In
Model.copy, remove the redundant cast around this.Id and read the declared
readonly Id property directly while preserving the existing trim, validation,
and copyModel behavior.
🤖 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.

Nitpick comments:
In `@modules/core/service/orm/model/model_copy.ts`:
- Around line 24-26: Remove the unused, non-exported isRelationType helper from
this file. Leave buildCopyBrowseSelection and buildCopyValues unchanged,
including their existing direct field.type checks.

In `@modules/core/service/orm/model/model.ts`:
- Around line 457-467: In Model.copy, remove the redundant cast around this.Id
and read the declared readonly Id property directly while preserving the
existing trim, validation, and copyModel behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: da05af50-aefb-40cb-a437-26ce3649e890

📥 Commits

Reviewing files that changed from the base of the PR and between bd64bff and 08769e7.

📒 Files selected for processing (13)
  • internal/module/artifact/generate/webapistore.go
  • internal/module/artifact/generate/webapistore.ts.tpl
  • internal/module/artifact/generate/webapistore_test.go
  • internal/parser/backendtsparser/field_resolved.go
  • internal/parser/backendtsparser/parser_migration_test.go
  • modules/core/service/orm/decorator/field.test.ts
  • modules/core/service/orm/decorator/field.ts
  • modules/core/service/orm/metadata/field.ts
  • modules/core/service/orm/model/model.ts
  • modules/core/service/orm/model/model_copy.test.ts
  • modules/core/service/orm/model/model_copy.ts
  • modules/core/service/orm/model/model_fields_get_facade.ts
  • pkg/meta/meta_field.go

- Remove unused isRelationType and dead undefined guard in copy helpers.

- Simplify instance copy Id access and emit copy:false via non-nil template check.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread modules/core/service/orm/model/model_copy.ts Fixed
@buke

buke commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

/improve

- Assign copyScalarOrManyToOneValue directly after the caller already skips undefined.

Co-authored-by: Cursor <cursoragent@cursor.com>
@buke

buke commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

/improve

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown

PR Code Suggestions ✨

Latest suggestions up to 652c7f3

CategorySuggestion                                                                                                                                    Impact
General
Safely handle null copy source rows

If row is null or non-object (for example if a browse query returns null), property
access row.Id and the subsequent in operator checks will throw a TypeError. Guard
row using asObjectRecord(row) before accessing row.Id or iterating field names.

modules/core/service/orm/model/model_copy.ts [141-154]

 export function buildCopyValues(
   ModelCtor: RuntimeModelCtor,
   row: ObjectRecord,
   defaults?: Partial<Record<string, unknown>>,
   state?: CopyWalkState
 ): UnknownRecord {
+  const sourceRecord = asObjectRecord(row);
+  if (!sourceRecord) {
+    return defaults ? { ...defaults } : {};
+  }
+
   const meta = getModelRuntimeMetadata(ModelCtor);
   const walk: CopyWalkState = state || {
     ancestorIds: new Set<string>(),
     depth: 0,
   };
 
-  const sourceId = extractRelationId(row.Id ?? row.id);
+  const sourceId = extractRelationId(sourceRecord.Id ?? sourceRecord.id);
   if (sourceId) walk.ancestorIds.add(sourceId);
Suggestion importance[1-10]: 6

__

Why: Guarding row with asObjectRecord before property access prevents potential TypeError runtime exceptions if row is null or non-object.

Low

Previous suggestions

Suggestions up to commit 1de33d8
CategorySuggestion                                                                                                                                    Impact
Possible issue
Return null for object relations missing IDs

When value is an object representing a ManyToOne relation but contains no valid ID
(such as {} or { Id: null }), extractRelationId returns null. Falling through
returns the raw object, causing record creation to fail when attempting to insert an
object payload into a foreign key column. Ensure object values always resolve to
either their string/number ID or null.

modules/core/service/orm/model/model_copy.ts [114-120]

 function copyScalarOrManyToOneValue(value: unknown): unknown {
-  if (value === null) return null;
-  const id = extractRelationId(value);
-  // Prefer Id when Browse nested a ManyToOne object; otherwise keep scalar as-is.
-  if (id != null && typeof value === 'object') return id;
+  if (value == null) return null;
+  if (typeof value === 'object') {
+    return extractRelationId(value);
+  }
   return value;
 }
Suggestion importance[1-10]: 7

__

Why: copyScalarOrManyToOneValue handles ManyToOne values during copy payload generation. If value is an object without a valid ID (such as {}), extractRelationId returns null, causing the function to fall back to returning the object itself instead of null. Returning an object for a ManyToOne foreign key field can break record creation.

Medium
Suggestions up to commit 08769e7
CategorySuggestion                                                                                                                                    Impact
Possible issue
Fix template rendering for pointer boolean

*In Go text/template, $field.Copy is a bool pointer set to &false when copy: false.
Evaluating if $field.Copy returns true for any non-nil pointer and printing
{{$field.Copy}} outputs a memory address, generating invalid TypeScript code. Use ne
$field.Copy nil to emit copy: false, when metadata is explicitly set to false.

internal/module/artifact/generate/webapistore.ts.tpl [51]

-{{- if $field.Copy}}copy: {{$field.Copy}},{{- end}}
+{{- if ne $field.Copy nil}}copy: false,{{- end}}
Suggestion importance[1-10]: 8

__

Why: In Go text/template, a non-nil *bool pointer (&false) evaluates to truthy in an if condition, and printing *bool directly outputs its pointer address, producing invalid TypeScript code.

Medium

buke and others added 3 commits July 25, 2026 19:47
- Avoid passing raw relation objects without Id into Create FK columns.

Co-authored-by: Cursor <cursoragent@cursor.com>
- Cover copy:true metadata, skip helpers, relation edge cases, and branch partials.

Co-authored-by: Cursor <cursoragent@cursor.com>
- Guard with asObjectRecord and throw instead of silently copying defaults.

Co-authored-by: Cursor <cursoragent@cursor.com>
@buke

buke commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

/improve

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown

PR Code Suggestions ✨

No code suggestions found for the PR.

- Skip auth password, token/session identity and lifecycle, and user token/session collections on Copy.

- Skip sequence watermark/idempotency keys and document mutation/upload request ids.

Co-authored-by: Cursor <cursoragent@cursor.com>
@buke
buke merged commit 23c5e2a into main Jul 25, 2026
74 of 76 checks passed
@buke
buke deleted the feat/orm-model-copy branch July 25, 2026 12:56
@coderabbitai coderabbitai Bot mentioned this pull request Jul 25, 2026
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant