Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: code completions for identity and built-in types #1268

Merged
merged 19 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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{
davenewza marked this conversation as resolved.
Show resolved Hide resolved
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
Loading