From e98af4d1266cdc2cc8f1dc35f471f90b6da5efb3 Mon Sep 17 00:00:00 2001 From: Matic Jurglic <matic@jurglic.si> Date: Thu, 30 Jan 2025 14:46:51 +0100 Subject: [PATCH] Add support for adding computed fields --- packages/base/command.gts | 1 + .../commands/add-field-to-card-definition.ts | 3 +- ...-field-to-card-definition-command-test.gts | 56 +++++++++++++++++-- .../realm-server/tests/module-syntax-test.ts | 47 ++++++++++++++++ packages/runtime-common/module-syntax.ts | 15 +++++ 5 files changed, 116 insertions(+), 6 deletions(-) diff --git a/packages/base/command.gts b/packages/base/command.gts index b9e65e54f8..4714fb3f03 100644 --- a/packages/base/command.gts +++ b/packages/base/command.gts @@ -135,6 +135,7 @@ export class AddFieldToCardDefinitionInput extends CardDef { @field outgoingRelativeTo = contains(StringField); // can be undefined when you know url is not going to be relative @field outgoingRealmURL = contains(StringField); // should be provided when the other 2 params are provided @field addFieldAtIndex = contains(NumberField); // if provided, the field will be added at the specified index in the card's possibleFields map + @field sourceCodeForComputedField = contains(StringField); // if provided, the field will be added as a computed field } export { diff --git a/packages/host/app/commands/add-field-to-card-definition.ts b/packages/host/app/commands/add-field-to-card-definition.ts index c533eeb1da..857b927f02 100644 --- a/packages/host/app/commands/add-field-to-card-definition.ts +++ b/packages/host/app/commands/add-field-to-card-definition.ts @@ -38,7 +38,7 @@ export default class AddFieldToCardDefinitionCommand extends HostBaseCommand< cardBeingModified: input.cardDefinitionToModify, fieldName: input.fieldName, fieldRef: input.fieldRef, - fieldType: input.fieldDefinitionType as FieldType, + fieldType: input.fieldType as FieldType, fieldDefinitionType: input.fieldDefinitionType as 'field' | 'card', incomingRelativeTo: input.incomingRelativeTo ? new URL(input.incomingRelativeTo) @@ -50,6 +50,7 @@ export default class AddFieldToCardDefinitionCommand extends HostBaseCommand< ? new URL(input.outgoingRealmURL) : undefined, addFieldAtIndex: input.addFieldAtIndex, + sourceCodeForComputedField: input.sourceCodeForComputedField, }); let writeTextFileCommand = new WriteTextFileCommand(this.commandContext); diff --git a/packages/host/tests/integration/commands/add-field-to-card-definition-command-test.gts b/packages/host/tests/integration/commands/add-field-to-card-definition-command-test.gts index c746f2aa0b..8d04e34c3b 100644 --- a/packages/host/tests/integration/commands/add-field-to-card-definition-command-test.gts +++ b/packages/host/tests/integration/commands/add-field-to-card-definition-command-test.gts @@ -49,10 +49,10 @@ module( contents: { 'person.gts': ` import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; - import StringCard from "https://cardstack.com/base/string"; + import StringField from "https://cardstack.com/base/string"; export class Person extends CardDef { static displayName = 'Person'; - @field firstName = contains(StringCard); + @field firstName = contains(StringField); } `, }, @@ -87,15 +87,61 @@ module( response, ` import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; - import StringCard from "https://cardstack.com/base/string"; + import StringField from "https://cardstack.com/base/string"; export class Person extends CardDef { static displayName = 'Person'; - @field firstName = contains(StringCard); - @field lastName = field(StringCard); + @field firstName = contains(StringField); + @field lastName = field(StringField); } `, 'lastName field was added to the card definition', ); }); + + test('can add a computed field', async function (assert) { + let commandService = lookupService<CommandService>('command-service'); + let cardService = lookupService<CardService>('card-service'); + let addFieldToCardDefinitionCommand = new AddFieldToCardDefinitionCommand( + commandService.commandContext, + ); + + await addFieldToCardDefinitionCommand.execute({ + cardDefinitionToModify: { + module: 'http://test-realm/test/person', + name: 'Person', + }, + fieldName: 'rapName', + fieldDefinitionType: 'field', + fieldType: 'contains', + fieldRef: { + module: 'https://cardstack.com/base/string', + name: 'default', + }, + incomingRelativeTo: undefined, + outgoingRelativeTo: undefined, + outgoingRealmURL: undefined, + sourceCodeForComputedField: '`Lil ${this.firstName}`;', + }); + + let response = await cardService.getSource( + new URL('person.gts', testRealmURL), + ); + assert.strictEqual( + response, + ` + import { contains, field, Component, CardDef } from "https://cardstack.com/base/card-api"; + import StringField from "https://cardstack.com/base/string"; + export class Person extends CardDef { + static displayName = 'Person'; + @field firstName = contains(StringField); + @field rapName = contains(StringField, { + computeVia: function () { + return \`Lil \${this.firstName}\`; + }, + }); + } + `, + ); + }); }, ); diff --git a/packages/realm-server/tests/module-syntax-test.ts b/packages/realm-server/tests/module-syntax-test.ts index 3a7eb5a71d..094ee8731a 100644 --- a/packages/realm-server/tests/module-syntax-test.ts +++ b/packages/realm-server/tests/module-syntax-test.ts @@ -853,6 +853,53 @@ module(basename(__filename), function () { ); }); + test('can add a contains field with a computed value', async function (assert) { + let src = ` + import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; + import StringField from "https://cardstack.com/base/string"; + + export class Person extends CardDef { + @field firstName = contains(StringField); + @field lastName = contains(StringField); + } + `; + + let mod = new ModuleSyntax(src, new URL(`${testRealm}dir/person`)); + mod.addField({ + cardBeingModified: { module: `${testRealm}dir/person`, name: 'Person' }, + fieldName: 'fullName', + fieldType: 'contains', + fieldDefinitionType: 'field', + fieldRef: { + module: 'https://cardstack.com/base/string', + name: 'default', + }, + incomingRelativeTo: undefined, + outgoingRelativeTo: undefined, + outgoingRealmURL: undefined, + sourceCodeForComputedField: + "[this.firstName, this.lastName].filter(Boolean).join(' ');", + }); + + assert.codeEqual( + mod.code(), + ` + import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; + import StringField from "https://cardstack.com/base/string"; + + export class Person extends CardDef { + @field firstName = contains(StringField); + @field lastName = contains(StringField); + @field fullName = contains(StringField, { + computeVia: function () { + return [this.firstName, this.lastName].filter(Boolean).join(' '); + }, + }); + } + `, + ); + }); + test('can handle field card declaration collisions when adding field', async function (assert) { let src = ` import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; diff --git a/packages/runtime-common/module-syntax.ts b/packages/runtime-common/module-syntax.ts index eba50d1bee..efe67c4a25 100644 --- a/packages/runtime-common/module-syntax.ts +++ b/packages/runtime-common/module-syntax.ts @@ -117,6 +117,7 @@ export class ModuleSyntax { outgoingRelativeTo, outgoingRealmURL, addFieldAtIndex, + sourceCodeForComputedField, }: { cardBeingModified: CodeRef; fieldName: string; @@ -127,6 +128,7 @@ export class ModuleSyntax { outgoingRelativeTo: URL | undefined; // can be undefined when you know url is not going to be relative outgoingRealmURL: URL | undefined; // should be provided when the other 2 params are provided addFieldAtIndex?: number; // if provided, the field will be added at the specified index in the card's possibleFields map + sourceCodeForComputedField?: string; // if provided, the field will be added as a computed field }) { let card = this.getCard(cardBeingModified); if (card.possibleFields.has(fieldName)) { @@ -147,6 +149,7 @@ export class ModuleSyntax { outgoingRelativeTo, outgoingRealmURL, moduleURL: this.url, + sourceCodeForComputedField, }); let src = this.code(); @@ -372,6 +375,7 @@ function makeNewField({ outgoingRelativeTo, outgoingRealmURL, moduleURL, + sourceCodeForComputedField, }: { target: NodePath<t.Node>; fieldRef: { name: string; module: string }; @@ -383,6 +387,7 @@ function makeNewField({ outgoingRelativeTo: URL | undefined; outgoingRealmURL: URL | undefined; moduleURL: URL; + sourceCodeForComputedField?: string; }): string { let programPath = getProgramPath(target); //@ts-ignore ImportUtil doesn't seem to believe our Babel.types is a @@ -395,6 +400,7 @@ function makeNewField({ `${baseRealm.url}card-api`, 'field', ); + debugger; let fieldTypeIdentifier = importUtil.import( target as NodePath<any>, `${baseRealm.url}card-api`, @@ -434,6 +440,15 @@ function makeNewField({ suggestedCardName(fieldRef, fieldDefinitionType), ); + if (sourceCodeForComputedField) { + debugger; + return `@${fieldDecorator.name} ${fieldName} = ${fieldTypeIdentifier.name}(${fieldCardIdentifier.name}, { + computeVia: function () { + return ${sourceCodeForComputedField} + }, + });`; + } + return `@${fieldDecorator.name} ${fieldName} = ${fieldTypeIdentifier.name}(${fieldCardIdentifier.name});`; }