Skip to content

Commit

Permalink
fix: code completions for identity and built-in types (#1268)
Browse files Browse the repository at this point in the history
  • Loading branch information
davenewza authored Oct 31, 2023
1 parent 9afd01f commit b6c59d9
Show file tree
Hide file tree
Showing 34 changed files with 1,480 additions and 317 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/publish_npm_packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ jobs:

- uses: actions/setup-node@v3
with:
node-version: 18
token: ${{ secrets.NPM_TOKEN }}
node-version: 18.12.1
token: ${{ secrets.NPM_TOKEN }}

- uses: pnpm/[email protected]
with:
version: 8.5.1
version: 8.10.0

- name: Checkout repository
uses: actions/checkout@v3
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test_npm_modules.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
node-version: 16.14.2 # vscode (electron) uses this so we want to make the tests use a comparable environment
- uses: pnpm/action-setup@v2
with:
version: 8.5.1
version: 8.10.0
- name: Setup golang
uses: actions/setup-go@v3
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/update_npm_packages_dist.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
steps:
- uses: actions/setup-node@v3
with:
node-version: 18
node-version: 18.12.1
token: ${{ secrets.NPM_TOKEN }}
- name: Adding `${{ inputs.publishTag }}` tag to ${{ matrix.package }}@${{ inputs.version }}
run: npm dist-tag add @teamkeel/${{ matrix.package }}@${{ inputs.version }} ${{ inputs.publishTag }}
Expand Down
2 changes: 1 addition & 1 deletion cmd/program/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type GenerateClientModel struct {

Err error
Schema *proto.Schema
SchemaFiles []reader.SchemaFile
SchemaFiles []*reader.SchemaFile
Secrets map[string]string
Config *config.ProjectConfig

Expand Down
2 changes: 1 addition & 1 deletion cmd/program/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func NextMsgCommand(ch chan tea.Msg) tea.Cmd {
type LoadSchemaMsg struct {
Schema *proto.Schema
Config *config.ProjectConfig
SchemaFiles []reader.SchemaFile
SchemaFiles []*reader.SchemaFile
Secrets map[string]string
Err error
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/program/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ type Model struct {
Err error
Schema *proto.Schema
Config *config.ProjectConfig
SchemaFiles []reader.SchemaFile
SchemaFiles []*reader.SchemaFile
Database db.Database
DatabaseConnInfo *db.ConnectionInfo
GeneratedFiles codegen.GeneratedFiles
Expand Down
51 changes: 51 additions & 0 deletions integration/testdata/set_backlinks/schema.keel
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,54 @@ model Record {
actions: [create, update]
)
}

model UserExtension {
fields {
name Text
identity1 Identity {
@unique
@relation(user1)
}
identity2 Identity {
@unique
@relation(user2)
}
user1 User
user2 User
email Text
isVerified Boolean
signedUpAt Timestamp
issuer Text
externalId Text
}

actions {
create createExt() with (n: Text) {
@set(userExtension.name = n)
@set(userExtension.identity1 = ctx.identity)
@set(userExtension.identity2.id = ctx.identity.id)
@set(userExtension.user1 = ctx.identity.user)
@set(userExtension.user2.id = ctx.identity.user.id)
@set(userExtension.email = ctx.identity.email)
@set(userExtension.isVerified = ctx.identity.emailVerified)
@set(userExtension.signedUpAt = ctx.identity.createdAt)
@set(userExtension.issuer = ctx.identity.issuer)
@set(userExtension.externalId = ctx.identity.externalId)
@permission(expression: ctx.isAuthenticated)
}

update updateExt(id) with (n: Text) {
@set(userExtension.name = n)
@set(userExtension.identity1 = ctx.identity)
@set(userExtension.identity2.id = ctx.identity.id)
@set(userExtension.user1 = ctx.identity.user)
@set(userExtension.user2.id = ctx.identity.user.id)
@set(userExtension.email = ctx.identity.email)
@set(userExtension.isVerified = ctx.identity.emailVerified)
@set(userExtension.signedUpAt = ctx.identity.createdAt)
@set(userExtension.issuer = ctx.identity.issuer)
@set(userExtension.externalId = ctx.identity.externalId)
@permission(expression: ctx.isAuthenticated)
}
}
}
88 changes: 88 additions & 0 deletions integration/testdata/set_backlinks/tests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,91 @@ test("update - @set with backlinks and no user backlink", async () => {
message: "field 'isActive' cannot be null",
});
});

test("create - @set with identity fields", async () => {
const { identityCreated } = await actions.authenticate({
createIfNotExists: true,
emailPassword: {
email: "[email protected]",
password: "1234",
},
});
expect(identityCreated).toBeTruthy();

const identity = await models.identity.update(
{ email: "[email protected]" },
{ externalId: "extId" }
);

const org = await models.organisation.create({
name: "Keel",
isActive: true,
});
const user = await models.user.create({
name: "Keelson",
identityId: identity!.id,
organisationId: org.id,
});

const extension = await actions
.withIdentity(identity!)
.createExt({ n: "Keelson" });

expect(extension.name).toEqual("Keelson");
expect(extension.identity1Id).toEqual(identity?.id);
expect(extension.identity2Id).toEqual(identity?.id);
expect(extension.user1Id).toEqual(user.id);
expect(extension.user2Id).toEqual(user.id);
expect(extension.email).toEqual(identity?.email);
expect(extension.isVerified).toEqual(identity?.emailVerified);
expect(extension.issuer).toEqual(identity?.issuer);
// https://linear.app/keel/issue/KE-1192/datetime-precision-loss
//expect(extension.signedUpAt).toEqual(identity?.createdAt);
expect(extension.externalId).toEqual(identity?.externalId);
});

test("update - @set with identity fields", async () => {
const { identityCreated } = await actions.authenticate({
createIfNotExists: true,
emailPassword: {
email: "[email protected]",
password: "1234",
},
});
expect(identityCreated).toBeTruthy();

const identity = await models.identity.update(
{ email: "[email protected]" },
{ externalId: "extId" }
);

const org = await models.organisation.create({
name: "Keel",
isActive: true,
});
const user = await models.user.create({
name: "Keelson",
identityId: identity!.id,
organisationId: org.id,
});

const { id } = await actions
.withIdentity(identity!)
.createExt({ n: "Keelson" });

const extension = await actions
.withIdentity(identity!)
.updateExt({ where: { id: id }, values: { n: "Keelson" } });

expect(extension.name).toEqual("Keelson");
expect(extension.identity1Id).toEqual(identity?.id);
expect(extension.identity2Id).toEqual(identity?.id);
expect(extension.user1Id).toEqual(user.id);
expect(extension.user2Id).toEqual(user.id);
expect(extension.email).toEqual(identity?.email);
expect(extension.isVerified).toEqual(identity?.emailVerified);
expect(extension.issuer).toEqual(identity?.issuer);
// https://linear.app/keel/issue/KE-1192/datetime-precision-loss
//expect(extension.signedUpAt).toEqual(identity?.createdAt);
expect(extension.externalId).toEqual(identity?.externalId);
});
4 changes: 2 additions & 2 deletions packages/wasm/lib/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,10 @@ func validate(this js.Value, args []js.Value) any {
return newPromise(func() (any, error) {

schemaFilesArg := args[0].Get("schemaFiles")
schemaFiles := []reader.SchemaFile{}
schemaFiles := []*reader.SchemaFile{}
for i := 0; i < schemaFilesArg.Length(); i++ {
f := schemaFilesArg.Index(i)
schemaFiles = append(schemaFiles, reader.SchemaFile{
schemaFiles = append(schemaFiles, &reader.SchemaFile{
FileName: f.Get("filename").String(),
Contents: f.Get("contents").String(),
})
Expand Down
84 changes: 84 additions & 0 deletions runtime/actions/query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2684,6 +2684,90 @@ var testCases = []testCase{
RETURNING "product"."id"`,
expectedArgs: []any{"prodcode", "brand", true, true},
},
{
name: "create_set_ctx_identity_fields",
keelSchema: `
model Person {
fields {
email Text
created Timestamp
emailVerified Boolean
externalId Text
issuer Text
}
actions {
create createPerson() {
@set(person.email = ctx.identity.email)
@set(person.created = ctx.identity.createdAt)
@set(person.emailVerified = ctx.identity.emailVerified)
@set(person.externalId = ctx.identity.externalId)
@set(person.issuer = ctx.identity.issuer)
}
}
@permission(expression: true, actions: [create])
}`,
actionName: "createPerson",
input: map[string]any{},
identity: identity,
expectedTemplate: `
WITH
select_identity (column_0, column_1, column_2, column_3, column_4) AS (
SELECT "identity"."email", "identity"."created_at", "identity"."email_verified", "identity"."external_id", "identity"."issuer"
FROM "identity"
WHERE "identity"."id" IS NOT DISTINCT FROM ?),
new_1_person AS (
INSERT INTO "person" (created, email, email_verified, external_id, issuer)
VALUES (
(SELECT column_1 FROM select_identity),
(SELECT column_0 FROM select_identity),
(SELECT column_2 FROM select_identity),
(SELECT column_3 FROM select_identity),
(SELECT column_4 FROM select_identity))
RETURNING *)
SELECT * FROM new_1_person`,
expectedArgs: []any{identity.Id},
},
{
name: "update_set_ctx_identity_fields",
keelSchema: `
model Person {
fields {
email Text
created Timestamp
emailVerified Boolean
externalId Text
issuer Text
}
actions {
update updatePerson(id) {
@set(person.email = ctx.identity.email)
@set(person.created = ctx.identity.createdAt)
@set(person.emailVerified = ctx.identity.emailVerified)
@set(person.externalId = ctx.identity.externalId)
@set(person.issuer = ctx.identity.issuer)
}
}
@permission(expression: true, actions: [create])
}`,
actionName: "updatePerson",
input: map[string]any{"where": map[string]any{"id": "xyz"}},
identity: identity,
expectedTemplate: `
WITH
select_identity (column_0, column_1, column_2, column_3, column_4) AS (
SELECT "identity"."email", "identity"."created_at", "identity"."email_verified", "identity"."external_id", "identity"."issuer"
FROM "identity"
WHERE "identity"."id" IS NOT DISTINCT FROM ?)
UPDATE "person" SET
created = (SELECT column_1 FROM select_identity),
email = (SELECT column_0 FROM select_identity),
email_verified = (SELECT column_2 FROM select_identity),
external_id = (SELECT column_3 FROM select_identity),
issuer = (SELECT column_4 FROM select_identity)
WHERE "person"."id" IS NOT DISTINCT FROM ?
RETURNING "person".*`,
expectedArgs: []any{identity.Id, "xyz"},
},
}

func TestQueryBuilder(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion runtime/apis/graphql/graphql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func TestGraphQL(t *testing.T) {
t.Run(name, func(t *testing.T) {
builder := schema.Builder{}
protoSchema, err := builder.MakeFromInputs(&reader.Inputs{
SchemaFiles: []reader.SchemaFile{
SchemaFiles: []*reader.SchemaFile{
{
Contents: tc.schema,
},
Expand Down
38 changes: 8 additions & 30 deletions runtime/expressions/operand.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,7 @@ func (resolver *OperandResolver) IsExplicitInput() bool {
// such as: @where(post.author.isActive)
func (resolver *OperandResolver) IsModelDbColumn() bool {
return !resolver.IsLiteral() &&
!resolver.IsContextField() &&
!resolver.IsContextDbColumn() &&
!resolver.IsContext() &&
!resolver.IsExplicitInput() &&
!resolver.IsImplicitInput()
}
Expand All @@ -175,7 +174,7 @@ func (resolver *OperandResolver) IsModelDbColumn() bool {
// which will require database access (such as with identity backlinks),
// such as: @permission(expression: ctx.identity.user.isActive)
func (resolver *OperandResolver) IsContextDbColumn() bool {
return resolver.operand.Ident.IsContext() && resolver.isContextIdentityDbColumn()
return resolver.operand.Ident.IsContextIdentity() && !resolver.operand.Ident.IsContextIdentityId()
}

// IsContextField returns true if the expression operand refers to a value on the context
Expand All @@ -189,7 +188,11 @@ func (resolver *OperandResolver) IsContextDbColumn() bool {
// then it returns false, because that can no longer be resolved solely from the
// in memory context data.
func (resolver *OperandResolver) IsContextField() bool {
return resolver.operand.Ident.IsContext() && !resolver.isContextIdentityDbColumn()
return resolver.operand.Ident.IsContext() && !resolver.IsContextDbColumn()
}

func (resolver *OperandResolver) IsContext() bool {
return resolver.operand.Ident.IsContext()
}

// GetOperandType returns the equivalent protobuf type for the expression operand.
Expand Down Expand Up @@ -322,7 +325,7 @@ func (resolver *OperandResolver) ResolveValue(args map[string]any) (any, error)
case resolver.IsModelDbColumn(), resolver.IsContextDbColumn():
// todo: https://linear.app/keel/issue/RUN-153/set-attribute-to-support-targeting-database-fields
panic("cannot resolve operand value from the database")
case resolver.operand.Ident.IsContextIdentityField():
case resolver.operand.Ident.IsContextIdentityId():
isAuthenticated := auth.IsAuthenticated(resolver.Context)
if !isAuthenticated {
return nil, nil
Expand Down Expand Up @@ -416,28 +419,3 @@ func toTime(s string) time.Time {
tm, _ := time.Parse(time.RFC3339, s)
return tm
}

// isContextIdentityDbColumn works out if this operand traverses an Identity field and requires the database.
func (resolver *OperandResolver) isContextIdentityDbColumn() bool {
if resolver.operand.Ident == nil {
return false
}
fragments := lo.Map(resolver.operand.Ident.Fragments, func(frag *parser.IdentFragment, _ int) string {
return frag.Fragment
})

if len(fragments) < 3 {
return false
}
if fragments[0] != "ctx" {
return false
}
if fragments[1] != "identity" {
return false
}
if fragments[2] == parser.ImplicitFieldNameId {
return false
}

return true
}
Loading

0 comments on commit b6c59d9

Please sign in to comment.