diff --git a/packages/runtime/test/policy/read.test.ts b/packages/runtime/test/policy/basic-schema-read.test.ts similarity index 100% rename from packages/runtime/test/policy/read.test.ts rename to packages/runtime/test/policy/basic-schema-read.test.ts diff --git a/packages/runtime/test/policy/auth.test.ts b/packages/runtime/test/policy/migrated/auth.test.ts similarity index 99% rename from packages/runtime/test/policy/auth.test.ts rename to packages/runtime/test/policy/migrated/auth.test.ts index f00c79e7..bc99f49f 100644 --- a/packages/runtime/test/policy/auth.test.ts +++ b/packages/runtime/test/policy/migrated/auth.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from './utils'; +import { createPolicyTestClient } from '../utils'; describe('auth() tests', () => { it('works with string id non-null test', async () => { diff --git a/packages/runtime/test/policy/client-extensions.test.ts b/packages/runtime/test/policy/migrated/client-extensions.test.ts similarity index 97% rename from packages/runtime/test/policy/client-extensions.test.ts rename to packages/runtime/test/policy/migrated/client-extensions.test.ts index 1f725172..16692543 100644 --- a/packages/runtime/test/policy/client-extensions.test.ts +++ b/packages/runtime/test/policy/migrated/client-extensions.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { definePlugin } from '../../src/client'; -import { createPolicyTestClient } from './utils'; +import { definePlugin } from '../../../src/client'; +import { createPolicyTestClient } from '../utils'; describe('client extensions tests for policies', () => { it('query override one model', async () => { diff --git a/packages/runtime/test/policy/connect-disconnect.test.ts b/packages/runtime/test/policy/migrated/connect-disconnect.test.ts similarity index 99% rename from packages/runtime/test/policy/connect-disconnect.test.ts rename to packages/runtime/test/policy/migrated/connect-disconnect.test.ts index d6e30128..02d8e04e 100644 --- a/packages/runtime/test/policy/connect-disconnect.test.ts +++ b/packages/runtime/test/policy/migrated/connect-disconnect.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from './utils'; +import { createPolicyTestClient } from '../utils'; describe('connect and disconnect tests', () => { const modelToMany = ` diff --git a/packages/runtime/test/policy/create-many-and-return.test.ts b/packages/runtime/test/policy/migrated/create-many-and-return.test.ts similarity index 98% rename from packages/runtime/test/policy/create-many-and-return.test.ts rename to packages/runtime/test/policy/migrated/create-many-and-return.test.ts index 97829ce4..1df0e5b6 100644 --- a/packages/runtime/test/policy/create-many-and-return.test.ts +++ b/packages/runtime/test/policy/migrated/create-many-and-return.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from './utils'; +import { createPolicyTestClient } from '../utils'; describe('createManyAndReturn tests', () => { it('works with model-level policies', async () => { diff --git a/packages/runtime/test/policy/cross-model-field-comparison.test.ts b/packages/runtime/test/policy/migrated/cross-model-field-comparison.test.ts similarity index 99% rename from packages/runtime/test/policy/cross-model-field-comparison.test.ts rename to packages/runtime/test/policy/migrated/cross-model-field-comparison.test.ts index 146992a2..f0a35f79 100644 --- a/packages/runtime/test/policy/cross-model-field-comparison.test.ts +++ b/packages/runtime/test/policy/migrated/cross-model-field-comparison.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from './utils'; +import { createPolicyTestClient } from '../utils'; describe('cross-model field comparison tests', () => { it('works with to-one relation', async () => { diff --git a/packages/runtime/test/policy/current-model.test.ts b/packages/runtime/test/policy/migrated/current-model.test.ts similarity index 99% rename from packages/runtime/test/policy/current-model.test.ts rename to packages/runtime/test/policy/migrated/current-model.test.ts index 024e658f..61ea1d24 100644 --- a/packages/runtime/test/policy/current-model.test.ts +++ b/packages/runtime/test/policy/migrated/current-model.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { createPolicyTestClient } from './utils'; +import { createPolicyTestClient } from '../utils'; describe('currentModel tests', () => { it('works in models', async () => { diff --git a/packages/runtime/test/policy/current-operation.test.ts b/packages/runtime/test/policy/migrated/current-operation.test.ts similarity index 98% rename from packages/runtime/test/policy/current-operation.test.ts rename to packages/runtime/test/policy/migrated/current-operation.test.ts index 957f8779..42d67939 100644 --- a/packages/runtime/test/policy/current-operation.test.ts +++ b/packages/runtime/test/policy/migrated/current-operation.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { createPolicyTestClient } from './utils'; +import { createPolicyTestClient } from '../utils'; describe('currentOperation tests', () => { it('works with specific rules', async () => { diff --git a/packages/runtime/test/policy/deep-nested.test.ts b/packages/runtime/test/policy/migrated/deep-nested.test.ts similarity index 99% rename from packages/runtime/test/policy/deep-nested.test.ts rename to packages/runtime/test/policy/migrated/deep-nested.test.ts index a35e34b8..bab02d5a 100644 --- a/packages/runtime/test/policy/deep-nested.test.ts +++ b/packages/runtime/test/policy/migrated/deep-nested.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from './utils'; +import { createPolicyTestClient } from '../utils'; describe('deep nested operations tests', () => { const model = ` diff --git a/packages/runtime/test/policy/empty-policy.test.ts b/packages/runtime/test/policy/migrated/empty-policy.test.ts similarity index 98% rename from packages/runtime/test/policy/empty-policy.test.ts rename to packages/runtime/test/policy/migrated/empty-policy.test.ts index 432454c1..452845b3 100644 --- a/packages/runtime/test/policy/empty-policy.test.ts +++ b/packages/runtime/test/policy/migrated/empty-policy.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from './utils'; +import { createPolicyTestClient } from '../utils'; describe('empty policy tests', () => { it('works with simple operations', async () => { diff --git a/packages/runtime/test/policy/field-comparison.test.ts b/packages/runtime/test/policy/migrated/field-comparison.test.ts similarity index 98% rename from packages/runtime/test/policy/field-comparison.test.ts rename to packages/runtime/test/policy/migrated/field-comparison.test.ts index 1d8e3cdf..1bf33c37 100644 --- a/packages/runtime/test/policy/field-comparison.test.ts +++ b/packages/runtime/test/policy/migrated/field-comparison.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from './utils'; +import { createPolicyTestClient } from '../utils'; describe('field comparison tests', () => { it('works with policies involving field comparison', async () => { diff --git a/packages/runtime/test/policy/multi-field-unique.test.ts b/packages/runtime/test/policy/migrated/multi-field-unique.test.ts similarity index 97% rename from packages/runtime/test/policy/multi-field-unique.test.ts rename to packages/runtime/test/policy/migrated/multi-field-unique.test.ts index 029bdaeb..7edbe019 100644 --- a/packages/runtime/test/policy/multi-field-unique.test.ts +++ b/packages/runtime/test/policy/migrated/multi-field-unique.test.ts @@ -1,9 +1,9 @@ import path from 'path'; import { afterEach, beforeAll, describe, expect, it } from 'vitest'; -import { createPolicyTestClient } from './utils'; -import { QueryError } from '../../src'; +import { createPolicyTestClient } from '../utils'; +import { QueryError } from '../../../src'; -describe('With Policy: multi-field unique', () => { +describe('Policy tests multi-field unique', () => { let origDir: string; beforeAll(async () => { diff --git a/packages/runtime/test/policy/migrated/multi-id-fields.test.ts b/packages/runtime/test/policy/migrated/multi-id-fields.test.ts new file mode 100644 index 00000000..56941f03 --- /dev/null +++ b/packages/runtime/test/policy/migrated/multi-id-fields.test.ts @@ -0,0 +1,395 @@ +import { describe, expect, it } from 'vitest'; +import { createPolicyTestClient } from '../utils'; + +describe('Policy tests multiple id fields', () => { + it('multi-id fields crud', async () => { + const db = await createPolicyTestClient( + ` + model A { + x String + y Int + value Int + b B? + @@id([x, y]) + + @@allow('read', true) + @@allow('create', value > 0) + } + + model B { + b1 String + b2 String + value Int + a A @relation(fields: [ax, ay], references: [x, y]) + ax String + ay Int + + @@allow('read', value > 2) + @@allow('create', value > 1) + + @@unique([ax, ay]) + @@id([b1, b2]) + } + `, + ); + + await expect(db.a.create({ data: { x: '1', y: 1, value: 0 } })).toBeRejectedByPolicy(); + await expect(db.a.create({ data: { x: '1', y: 2, value: 1 } })).toResolveTruthy(); + + await expect( + db.a.create({ data: { x: '2', y: 1, value: 1, b: { create: { b1: '1', b2: '2', value: 1 } } } }), + ).toBeRejectedByPolicy(); + + const r = await db.a.create({ + include: { b: true }, + data: { x: '2', y: 1, value: 1, b: { create: { b1: '1', b2: '2', value: 2 } } }, + }); + expect(r.b).toBeNull(); + + const r1 = await db.$unuseAll().b.findUnique({ where: { b1_b2: { b1: '1', b2: '2' } } }); + expect(r1.value).toBe(2); + + await expect( + db.a.create({ + include: { b: true }, + data: { x: '3', y: 1, value: 1, b: { create: { b1: '2', b2: '2', value: 3 } } }, + }), + ).toResolveTruthy(); + }); + + // TODO: `future()` support + it.skip('multi-id fields id update', async () => { + const db = await createPolicyTestClient( + ` + model A { + x String + y Int + value Int + b B? + @@id([x, y]) + + @@allow('read', true) + @@allow('create', value > 0) + @@allow('update', value > 0 && future().value > 1) + } + + model B { + b1 String + b2 String + value Int + a A @relation(fields: [ax, ay], references: [x, y]) + ax String + ay Int + + @@allow('read', value > 2) + @@allow('create', value > 1) + + @@unique([ax, ay]) + @@id([b1, b2]) + } + `, + ); + + await db.a.create({ data: { x: '1', y: 2, value: 1 } }); + + await expect( + db.a.update({ where: { x_y: { x: '1', y: 2 } }, data: { x: '2', y: 3, value: 0 } }), + ).toBeRejectedByPolicy(); + + await expect( + db.a.update({ where: { x_y: { x: '1', y: 2 } }, data: { x: '2', y: 3, value: 2 } }), + ).resolves.toMatchObject({ + x: '2', + y: 3, + value: 2, + }); + + await expect( + db.a.upsert({ + where: { x_y: { x: '2', y: 3 } }, + update: { x: '3', y: 4, value: 0 }, + create: { x: '4', y: 5, value: 5 }, + }), + ).toBeRejectedByPolicy(); + + await expect( + db.a.upsert({ + where: { x_y: { x: '2', y: 3 } }, + update: { x: '3', y: 4, value: 3 }, + create: { x: '4', y: 5, value: 5 }, + }), + ).resolves.toMatchObject({ + x: '3', + y: 4, + value: 3, + }); + }); + + it('multi-id auth', async () => { + const db = await createPolicyTestClient( + ` + model User { + x String + y String + m M? + n N? + p P? + q Q? + @@id([x, y]) + @@allow('all', true) + } + + model M { + id String @id @default(cuid()) + owner User @relation(fields: [ownerX, ownerY], references: [x, y]) + ownerX String + ownerY String + @@unique([ownerX, ownerY]) + @@allow('all', auth() == owner) + } + + model N { + id String @id @default(cuid()) + owner User @relation(fields: [ownerX, ownerY], references: [x, y]) + ownerX String + ownerY String + @@unique([ownerX, ownerY]) + @@allow('all', auth().x == owner.x && auth().y == owner.y) + } + + model P { + id String @id @default(cuid()) + owner User @relation(fields: [ownerX, ownerY], references: [x, y]) + ownerX String + ownerY String + @@unique([ownerX, ownerY]) + @@allow('all', auth() != owner) + } + + model Q { + id String @id @default(cuid()) + owner User @relation(fields: [ownerX, ownerY], references: [x, y]) + ownerX String + ownerY String + @@unique([ownerX, ownerY]) + @@allow('all', auth() != null) + } + `, + ); + + await db.$unuseAll().user.create({ data: { x: '1', y: '1' } }); + await db.$unuseAll().user.create({ data: { x: '1', y: '2' } }); + + await expect(db.m.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } })).toBeRejectedByPolicy(); + await expect(db.m.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } })).toBeRejectedByPolicy(); + await expect(db.n.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } })).toBeRejectedByPolicy(); + await expect(db.n.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } })).toBeRejectedByPolicy(); + + const dbAuth = db.$setAuth({ x: '1', y: '1' }); + + await expect( + dbAuth.m.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } }), + ).toBeRejectedByPolicy(); + await expect(dbAuth.m.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } })).toResolveTruthy(); + await expect( + dbAuth.n.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } }), + ).toBeRejectedByPolicy(); + await expect(dbAuth.n.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } })).toResolveTruthy(); + await expect( + dbAuth.p.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } }), + ).toBeRejectedByPolicy(); + await expect(dbAuth.p.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } })).toResolveTruthy(); + + await expect(db.q.create({ data: { owner: { connect: { x_y: { x: '1', y: '1' } } } } })).toBeRejectedByPolicy(); + await expect(dbAuth.q.create({ data: { owner: { connect: { x_y: { x: '1', y: '2' } } } } })).toResolveTruthy(); + }); + + it('multi-id to-one nested write', async () => { + const db = await createPolicyTestClient( + ` + model A { + x Int + y Int + v Int + b B @relation(fields: [bId], references: [id]) + bId Int @unique + + @@id([x, y]) + @@allow('all', v > 0) + } + + model B { + id Int @id + v Int + a A? + + @@allow('all', v > 0) + } + `, + ); + await expect( + db.b.create({ + data: { + id: 1, + v: 1, + a: { + create: { + x: 1, + y: 2, + v: 3, + }, + }, + }, + }), + ).toResolveTruthy(); + + await expect( + db.a.update({ + where: { x_y: { x: 1, y: 2 } }, + data: { b: { update: { v: 5 } } }, + }), + ).toResolveTruthy(); + + expect(await db.b.findUnique({ where: { id: 1 } })).toEqual(expect.objectContaining({ v: 5 })); + }); + + it('multi-id to-many nested write', async () => { + const db = await createPolicyTestClient( + ` + model A { + x Int + y Int + v Int + b B @relation(fields: [bId], references: [id]) + bId Int @unique + + @@id([x, y]) + @@allow('all', v > 0) + } + + model B { + id Int @id + v Int + a A[] + c C? + + @@allow('all', v > 0) + } + + model C { + id Int @id + v Int + b B @relation(fields: [bId], references: [id]) + bId Int @unique + + @@allow('all', v > 0) + } + `, + ); + await expect( + db.b.create({ + data: { + id: 1, + v: 1, + a: { + create: { + x: 1, + y: 2, + v: 2, + }, + }, + c: { + create: { + id: 1, + v: 3, + }, + }, + }, + }), + ).toResolveTruthy(); + + await expect( + db.a.update({ + where: { x_y: { x: 1, y: 2 } }, + data: { b: { update: { v: 5, c: { update: { v: 6 } } } } }, + }), + ).toResolveTruthy(); + + expect(await db.b.findUnique({ where: { id: 1 } })).toEqual(expect.objectContaining({ v: 5 })); + expect(await db.c.findUnique({ where: { id: 1 } })).toEqual(expect.objectContaining({ v: 6 })); + }); + + // TODO: `future()` support + it.skip('multi-id fields nested id update', async () => { + const db = await createPolicyTestClient( + ` + model A { + x String + y Int + value Int + b B @relation(fields: [bId], references: [id]) + bId Int + @@id([x, y]) + + @@allow('read', true) + @@allow('create', value > 0) + @@allow('update', value > 0 && future().value > 1) + } + + model B { + id Int @id @default(autoincrement()) + a A[] + @@allow('all', true) + } + `, + ); + + await db.b.create({ data: { id: 1, a: { create: { x: '1', y: 1, value: 1 } } } }); + + await expect( + db.b.update({ + where: { id: 1 }, + data: { a: { update: { where: { x_y: { x: '1', y: 1 } }, data: { x: '2', y: 2, value: 0 } } } }, + }), + ).toBeRejectedByPolicy(); + + await expect( + db.b.update({ + where: { id: 1 }, + data: { a: { update: { where: { x_y: { x: '1', y: 1 } }, data: { x: '2', y: 2, value: 2 } } } }, + include: { a: true }, + }), + ).resolves.toMatchObject({ a: expect.arrayContaining([expect.objectContaining({ x: '2', y: 2, value: 2 })]) }); + + await expect( + db.b.update({ + where: { id: 1 }, + data: { + a: { + upsert: { + where: { x_y: { x: '2', y: 2 } }, + update: { x: '3', y: 3, value: 0 }, + create: { x: '4', y: '4', value: 4 }, + }, + }, + }, + }), + ).toBeRejectedByPolicy(); + + await expect( + db.b.update({ + where: { id: 1 }, + data: { + a: { + upsert: { + where: { x_y: { x: '2', y: 2 } }, + update: { x: '3', y: 3, value: 3 }, + create: { x: '4', y: '4', value: 4 }, + }, + }, + }, + include: { a: true }, + }), + ).resolves.toMatchObject({ a: expect.arrayContaining([expect.objectContaining({ x: '3', y: 3, value: 3 })]) }); + }); +}); diff --git a/packages/runtime/test/policy/migrated/nested-to-many.test.ts b/packages/runtime/test/policy/migrated/nested-to-many.test.ts new file mode 100644 index 00000000..63d72821 --- /dev/null +++ b/packages/runtime/test/policy/migrated/nested-to-many.test.ts @@ -0,0 +1,720 @@ +import { describe, expect, it } from 'vitest'; +import { createPolicyTestClient } from '../utils'; + +describe('Policy tests to-many', () => { + it('read filtering', async () => { + const db = await createPolicyTestClient( + ` + model M1 { + id String @id @default(uuid()) + m2 M2[] + + @@allow('all', true) + } + + model M2 { + id String @id @default(uuid()) + value Int + m1 M1 @relation(fields: [m1Id], references:[id]) + m1Id String + + @@allow('create', true) + @@allow('read', value > 0) + } + `, + ); + + let read = await db.m1.create({ + include: { m2: true }, + data: { + id: '1', + m2: { + create: [{ value: 0 }], + }, + }, + }); + expect(read.m2).toHaveLength(0); + read = await db.m1.findFirst({ where: { id: '1' }, include: { m2: true } }); + expect(read.m2).toHaveLength(0); + + await db.m1.create({ + data: { + id: '2', + m2: { + create: [{ value: 0 }, { value: 1 }, { value: 2 }], + }, + }, + }); + read = await db.m1.findFirst({ where: { id: '2' }, include: { m2: true } }); + expect(read.m2).toHaveLength(2); + }); + + // TODO: do we need to keep the v2 semantic? + it.skip('read condition hoisting', async () => { + const db = await createPolicyTestClient( + ` + model M1 { + id String @id @default(uuid()) + m2 M2[] + + @@allow('all', true) + } + + model M2 { + id String @id @default(uuid()) + value Int + + m1 M1 @relation(fields: [m1Id], references:[id]) + m1Id String + + m3 M3 @relation(fields: [m3Id], references:[id]) + m3Id String @unique + + m4 M4 @relation(fields: [m4Id], references:[id]) + m4Id String + + @@allow('create', true) + @@allow('read', value > 0) + } + + model M3 { + id String @id @default(uuid()) + value Int + m2 M2? + + @@allow('create', true) + @@allow('read', value > 1) + } + + model M4 { + id String @id @default(uuid()) + value Int + m2 M2[] + + @@allow('create', true) + @@allow('read', value > 1) + } + `, + ); + + await db.m1.create({ + include: { m2: true }, + data: { + id: '1', + m2: { + create: [ + { id: 'm2-1', value: 1, m3: { create: { value: 1 } }, m4: { create: { value: 1 } } }, + { id: 'm2-2', value: 1, m3: { create: { value: 2 } }, m4: { create: { value: 2 } } }, + ], + }, + }, + }); + + let read = await db.m1.findFirst({ include: { m2: true } }); + expect(read.m2).toHaveLength(2); + read = await db.m1.findFirst({ select: { m2: { select: { id: true } } } }); + expect(read.m2).toHaveLength(2); + + // check m2-m3 filtering + // including m3 causes m2 to be filtered since m3 is not nullable + read = await db.m1.findFirst({ include: { m2: { include: { m3: true } } } }); + expect(read.m2).toHaveLength(1); + read = await db.m1.findFirst({ select: { m2: { select: { m3: true } } } }); + expect(read.m2).toHaveLength(1); + + // check m2-m4 filtering + // including m3 causes m2 to be filtered since m4 is not nullable + read = await db.m1.findFirst({ include: { m2: { include: { m4: true } } } }); + expect(read.m2).toHaveLength(1); + read = await db.m1.findFirst({ select: { m2: { select: { m4: true } } } }); + expect(read.m2).toHaveLength(1); + }); + + it('create simple', async () => { + const db = await createPolicyTestClient( + ` + model M1 { + id String @id @default(uuid()) + m2 M2[] + + @@allow('all', true) + } + + model M2 { + id String @id @default(uuid()) + value Int + m1 M1 @relation(fields: [m1Id], references:[id]) + m1Id String + + @@allow('read', true) + @@allow('create', value > 0) + } + `, + ); + + // single create denied + await expect( + db.m1.create({ + data: { + m2: { + create: { value: 0 }, + }, + }, + }), + ).toBeRejectedByPolicy(); + + await expect( + db.m1.create({ + data: { + m2: { + create: { value: 1 }, + }, + }, + }), + ).toResolveTruthy(); + + // multi create denied + await expect( + db.m1.create({ + data: { + m2: { + create: [{ value: 0 }, { value: 1 }], + }, + }, + }), + ).toBeRejectedByPolicy(); + + await expect( + db.m1.create({ + data: { + m2: { + create: [{ value: 1 }, { value: 2 }], + }, + }, + }), + ).toResolveTruthy(); + }); + + it('update simple', async () => { + const db = await createPolicyTestClient( + ` + model M1 { + id String @id @default(uuid()) + m2 M2[] + + @@allow('all', true) + } + + model M2 { + id String @id @default(uuid()) + value Int + m1 M1 @relation(fields: [m1Id], references:[id]) + m1Id String + + @@allow('read', true) + @@allow('create', true) + @@allow('update', value > 1) + } + `, + ); + + await db.m1.create({ + data: { + id: '1', + m2: { + create: [{ id: '1', value: 1 }], + }, + }, + }); + + // update denied + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + update: { + where: { id: '1' }, + data: { value: 2 }, + }, + }, + }, + }), + ).toBeRejectedNotFound(); + + await db.m1.create({ + data: { + id: '2', + m2: { + create: { id: '2', value: 2 }, + }, + }, + }); + + // update success + const r = await db.m1.update({ + where: { id: '2' }, + include: { m2: true }, + data: { + m2: { + update: { + where: { id: '2' }, + data: { value: 3 }, + }, + }, + }, + }); + expect(r.m2).toEqual(expect.arrayContaining([expect.objectContaining({ id: '2', value: 3 })])); + }); + + it('update id field', async () => { + const db = await createPolicyTestClient( + ` + model M1 { + id String @id @default(uuid()) + m2 M2[] + + @@allow('all', true) + } + + model M2 { + id String @id @default(uuid()) + value Int + m1 M1 @relation(fields: [m1Id], references:[id]) + m1Id String + + @@allow('read', true) + @@allow('create', true) + @@allow('update', value > 1) + } + `, + ); + + await db.m1.create({ + data: { + id: '1', + m2: { + create: { id: '1', value: 2 }, + }, + }, + }); + + let r = await db.m1.update({ + where: { id: '1' }, + include: { m2: true }, + data: { + m2: { + update: { + where: { id: '1' }, + data: { id: '2', value: 3 }, + }, + }, + }, + }); + expect(r.m2).toEqual(expect.arrayContaining([expect.objectContaining({ id: '2', value: 3 })])); + + r = await db.m1.update({ + where: { id: '1' }, + include: { m2: true }, + data: { + m2: { + upsert: { + where: { id: '2' }, + create: { id: '4', value: 4 }, + update: { id: '3', value: 4 }, + }, + }, + }, + }); + expect(r.m2).toEqual(expect.arrayContaining([expect.objectContaining({ id: '3', value: 4 })])); + }); + + it('update with create from one to many', async () => { + const db = await createPolicyTestClient( + ` + model M1 { + id String @id @default(uuid()) + m2 M2[] + + @@allow('all', true) + } + + model M2 { + id String @id @default(uuid()) + value Int + m1 M1 @relation(fields: [m1Id], references:[id]) + m1Id String + + @@allow('read', true) + @@allow('create', value > 0) + @@allow('update', value > 1) + } + `, + ); + + await db.m1.create({ + data: { + id: '1', + m2: { + create: { value: 1 }, + }, + }, + }); + + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + create: [{ value: 0 }, { value: 1 }], + }, + }, + }), + ).toBeRejectedByPolicy(); + + const r = await db.m1.update({ + where: { id: '1' }, + include: { m2: true }, + data: { + m2: { + create: [{ value: 1 }, { value: 2 }], + }, + }, + }); + expect(r.m2).toHaveLength(3); + }); + + it('update with create from many to one', async () => { + const db = await createPolicyTestClient( + ` + model M1 { + id String @id @default(uuid()) + value Int + m2 M2[] + + @@allow('read', true) + @@allow('create', value > 0) + @@allow('update', value > 1) + } + + model M2 { + id String @id @default(uuid()) + m1 M1? @relation(fields: [m1Id], references:[id]) + m1Id String? + + @@allow('all', true) + } + `, + ); + + await db.m2.create({ data: { id: '1' } }); + + await expect( + db.m2.update({ + where: { id: '1' }, + data: { + m1: { + create: { value: 0 }, + }, + }, + }), + ).toBeRejectedByPolicy(); + + await expect( + db.m2.update({ + where: { id: '1' }, + data: { + m1: { + create: { value: 1 }, + }, + }, + }), + ).toResolveTruthy(); + }); + + it('update with delete', async () => { + const db = await createPolicyTestClient( + ` + model M1 { + id String @id @default(uuid()) + m2 M2[] + + @@allow('all', true) + } + + model M2 { + id String @id @default(uuid()) + value Int + m1 M1 @relation(fields: [m1Id], references:[id]) + m1Id String + + @@allow('read', true) + @@allow('create', value > 0) + @@allow('update', value > 1) + @@allow('delete', value > 2) + } + `, + ); + + await db.m1.create({ + data: { + id: '1', + m2: { + create: [ + { id: '1', value: 1 }, + { id: '2', value: 2 }, + { id: '3', value: 3 }, + { id: '4', value: 4 }, + { id: '5', value: 5 }, + ], + }, + }, + }); + + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + delete: { id: '1' }, + }, + }, + }), + ).toBeRejectedNotFound(); + expect(await db.$unuseAll().m2.findMany()).toHaveLength(5); + + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + delete: [{ id: '1' }, { id: '2' }], + }, + }, + }), + ).toBeRejectedNotFound(); + expect(await db.$unuseAll().m2.findMany()).toHaveLength(5); + + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + deleteMany: { OR: [{ id: '2' }, { id: '3' }] }, + }, + }, + }), + ).toResolveTruthy(); + // only m2#3 should be deleted, m2#2 should remain because of policy + await expect(db.m2.findUnique({ where: { id: '3' } })).toResolveNull(); + await expect(db.m2.findUnique({ where: { id: '2' } })).toResolveTruthy(); + + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + delete: { id: '3' }, + }, + }, + }), + ).toBeRejectedNotFound(); + + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + deleteMany: { value: { gte: 4 } }, + }, + }, + }), + ).toResolveTruthy(); + + await expect(db.m2.findMany({ where: { id: { in: ['4', '5'] } } })).resolves.toHaveLength(0); + }); + + it('create with nested read', async () => { + const db = await createPolicyTestClient( + ` + model M1 { + id String @id @default(uuid()) + value Int + m2 M2[] + m3 M3? + + @@allow('read', value > 1) + @@allow('create', true) + } + + model M2 { + id String @id @default(uuid()) + value Int + m1 M1 @relation(fields: [m1Id], references:[id]) + m1Id String + + @@allow('create', true) + @@allow('read', value > 0) + } + + model M3 { + id String @id @default(uuid()) + value Int + m1 M1 @relation(fields: [m1Id], references:[id]) + m1Id String @unique + + @@allow('create', true) + @@allow('read', value > 0) + } + `, + ); + + await expect( + db.m1.create({ + data: { + id: '1', + value: 1, + }, + }), + ).toBeRejectedByPolicy(); + + // included 'm1' can't be read + await expect( + db.m2.create({ + include: { m1: true }, + data: { + id: '1', + value: 1, + m1: { connect: { id: '1' } }, + }, + }), + ).resolves.toMatchObject({ m1: null }); + await expect(db.m2.findUnique({ where: { id: '1' } })).toResolveTruthy(); + + // included 'm1' can't be read + await expect( + db.m3.create({ + include: { m1: true }, + data: { + id: '1', + value: 1, + m1: { connect: { id: '1' } }, + }, + }), + ).resolves.toMatchObject({ m1: null }); + await expect(db.m3.findUnique({ where: { id: '1' } })).toResolveTruthy(); + + // nested to-many got filtered on read + const r = await db.m1.create({ + include: { m2: true }, + data: { + value: 2, + m2: { create: [{ value: 0 }, { value: 1 }] }, + }, + }); + expect(r.m2).toHaveLength(1); + + // read-back for to-one relation rejected + const r1 = await db.m1.create({ + include: { m3: true }, + data: { + value: 2, + m3: { create: { value: 0 } }, + }, + }); + expect(r1.m3).toBeNull(); + }); + + it('update with nested read', async () => { + const db = await createPolicyTestClient( + ` + model M1 { + id String @id @default(uuid()) + m2 M2[] + m3 M3? + + @@allow('all', true) + } + + model M2 { + id String @id @default(uuid()) + value Int + m1 M1 @relation(fields: [m1Id], references:[id]) + m1Id String + + @@allow('read', value > 1) + @@allow('create,update', true) + } + + model M3 { + id String @id @default(uuid()) + value Int + m1 M1 @relation(fields: [m1Id], references:[id]) + m1Id String @unique + + @@allow('read', value > 1) + @@allow('create,update', true) + } + `, + ); + + await db.m1.create({ + data: { + id: '1', + m2: { + create: [ + { id: '1', value: 0 }, + { id: '2', value: 0 }, + ], + }, + m3: { + create: { value: 0 }, + }, + }, + }); + + const r = await db.m1.update({ + where: { id: '1' }, + include: { m3: true }, + data: { + m3: { + update: { + value: 1, + }, + }, + }, + }); + expect(r.m3).toBeNull(); + + const r1 = await db.m1.update({ + where: { id: '1' }, + include: { m3: true, m2: true }, + data: { + m3: { + update: { + value: 2, + }, + }, + }, + }); + // m3 is ok now + expect(r1.m3.value).toBe(2); + // m2 got filtered + expect(r1.m2).toHaveLength(0); + + const r2 = await db.m1.update({ + where: { id: '1' }, + select: { m2: true }, + data: { + m2: { + update: { + where: { id: '1' }, + data: { value: 2 }, + }, + }, + }, + }); + // one of m2 matches policy now + expect(r2.m2).toHaveLength(1); + }); +}); diff --git a/packages/runtime/test/policy/migrated/nested-to-one.test.ts b/packages/runtime/test/policy/migrated/nested-to-one.test.ts new file mode 100644 index 00000000..5838cae8 --- /dev/null +++ b/packages/runtime/test/policy/migrated/nested-to-one.test.ts @@ -0,0 +1,445 @@ +import { describe, it, expect } from 'vitest'; +import { createPolicyTestClient } from '../utils'; + +describe('With Policy:nested to-one', () => { + it('read filtering for optional relation', async () => { + const db = await createPolicyTestClient( + ` + model M1 { + id String @id @default(uuid()) + m2 M2? + + @@allow('all', true) + } + + model M2 { + id String @id @default(uuid()) + m1 M1 @relation(fields: [m1Id], references:[id]) + m1Id String @unique + value Int + + @@allow('create', true) + @@allow('read', value > 0) + } + `, + ); + + let read = await db.m1.create({ + include: { m2: true }, + data: { + id: '1', + m2: { + create: { id: '1', value: 0 }, + }, + }, + }); + expect(read.m2).toBeNull(); + + await expect(db.m1.findUnique({ where: { id: '1' }, include: { m2: true } })).resolves.toEqual( + expect.objectContaining({ m2: null }), + ); + await expect(db.m1.findMany({ include: { m2: true } })).resolves.toEqual( + expect.arrayContaining([expect.objectContaining({ m2: null })]), + ); + + await db.$unuseAll().m2.update({ where: { id: '1' }, data: { value: 1 } }); + read = await db.m1.findUnique({ where: { id: '1' }, include: { m2: true } }); + expect(read.m2).toEqual(expect.objectContaining({ id: '1', value: 1 })); + }); + + it('read rejection for non-optional relation', async () => { + const db = await createPolicyTestClient( + ` + model M1 { + id String @id @default(uuid()) + m2 M2? + value Int + + @@allow('create', true) + @@allow('read', value > 0) + } + + model M2 { + id String @id @default(uuid()) + m1 M1 @relation(fields: [m1Id], references:[id]) + m1Id String @unique + + @@allow('all', true) + } + `, + ); + + await db.$unuseAll().m1.create({ + data: { + id: '1', + value: 0, + m2: { + create: { id: '1' }, + }, + }, + }); + + await expect(db.m2.findUnique({ where: { id: '1' }, include: { m1: true } })).resolves.toMatchObject({ + m1: null, + }); + + await db.$unuseAll().m1.update({ where: { id: '1' }, data: { value: 1 } }); + await expect(db.m2.findMany({ include: { m1: true } })).toResolveTruthy(); + }); + + // TODO: should we keep v2 semantic? + it.skip('read condition hoisting', async () => { + const db = await createPolicyTestClient( + ` + model M1 { + id String @id @default(uuid()) + m2 M2 @relation(fields: [m2Id], references:[id]) + m2Id String @unique + + @@allow('all', true) + } + + model M2 { + id String @id @default(uuid()) + value Int + + m1 M1? + + m3 M3 @relation(fields: [m3Id], references:[id]) + m3Id String @unique + + @@allow('create', true) + @@allow('read', value > 0) + } + + model M3 { + id String @id @default(uuid()) + value Int + m2 M2? + + @@allow('create', true) + @@allow('read', value > 1) + } + `, + ); + + await db.m1.create({ + include: { m2: true }, + data: { + id: '1', + m2: { + create: { id: 'm2-1', value: 1, m3: { create: { value: 1 } } }, + }, + }, + }); + + // check m2-m3 filtering + // including m3 causes m1 to be filtered due to hosting + await expect(db.m1.findFirst({ include: { m2: { include: { m3: true } } } })).toResolveNull(); + await expect(db.m1.findFirst({ select: { m2: { select: { m3: true } } } })).toResolveNull(); + }); + + it('create and update tests', async () => { + const db = await createPolicyTestClient( + ` + model M1 { + id String @id @default(uuid()) + m2 M2? + + @@allow('all', true) + } + + model M2 { + id String @id @default(uuid()) + value Int + m1 M1 @relation(fields: [m1Id], references:[id]) + m1Id String @unique + + @@allow('read', true) + @@allow('create', value > 0) + @@allow('update', value > 1) + } + `, + ); + + // create denied + await expect( + db.m1.create({ + data: { + m2: { + create: { value: 0 }, + }, + }, + }), + ).toBeRejectedByPolicy(); + + await expect( + db.m1.create({ + data: { + id: '1', + m2: { + create: { id: '1', value: 1 }, + }, + }, + }), + ).toResolveTruthy(); + + // nested update denied + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + update: { value: 2 }, + }, + }, + }), + ).toBeRejectedNotFound(); + }); + + // TODO: `future()` support + it.skip('nested update id tests', async () => { + const db = await createPolicyTestClient( + ` + model M1 { + id String @id @default(uuid()) + m2 M2? + + @@allow('all', true) + } + + model M2 { + id String @id @default(uuid()) + value Int + m1 M1 @relation(fields: [m1Id], references:[id]) + m1Id String @unique + + @@allow('read', true) + @@allow('create', value > 0) + @@allow('update', value > 1 && future().value > 2) + } + `, + ); + + await db.m1.create({ + data: { + id: '1', + m2: { + create: { id: '1', value: 2 }, + }, + }, + }); + + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + update: { id: '2', value: 1 }, + }, + }, + }), + ).toBeRejectedByPolicy(); + + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + update: { id: '2', value: 3 }, + }, + }, + include: { m2: true }, + }), + ).resolves.toMatchObject({ m2: expect.objectContaining({ id: '2', value: 3 }) }); + }); + + it('nested create', async () => { + const db = await createPolicyTestClient( + ` + model M1 { + id String @id @default(uuid()) + m2 M2? + + @@allow('all', true) + } + + model M2 { + id String @id @default(uuid()) + value Int + m1 M1 @relation(fields: [m1Id], references:[id]) + m1Id String @unique + + @@allow('read', true) + @@allow('create', value > 0) + @@allow('update', value > 1) + } + `, + ); + + await db.m1.create({ + data: { + id: '1', + }, + }); + + // nested create denied + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + create: { value: 0 }, + }, + }, + }), + ).toBeRejectedByPolicy(); + + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + create: { value: 1 }, + }, + }, + }), + ).toResolveTruthy(); + }); + + it('nested delete', async () => { + const db = await createPolicyTestClient( + ` + model M1 { + id String @id @default(uuid()) + m2 M2? + + @@allow('all', true) + } + + model M2 { + id String @id @default(uuid()) + value Int + m1 M1 @relation(fields: [m1Id], references:[id]) + m1Id String @unique + + @@allow('read', true) + @@allow('create', true) + @@allow('update', true) + @@allow('delete', value > 1) + } + `, + ); + + await db.m1.create({ + data: { + id: '1', + m2: { + create: { id: '1', value: 1 }, + }, + }, + }); + + // nested delete denied + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { delete: true }, + }, + }), + ).toBeRejectedNotFound(); + expect(await db.m2.findUnique({ where: { id: '1' } })).toBeTruthy(); + + // update m2 so it can be deleted + await db.m1.update({ + where: { id: '1' }, + data: { + m2: { update: { value: 3 } }, + }, + }); + + expect( + await db.m1.update({ + where: { id: '1' }, + data: { + m2: { delete: true }, + }, + }), + ).toBeTruthy(); + // check deleted + expect(await db.m2.findUnique({ where: { id: '1' } })).toBeNull(); + }); + + it('nested relation delete', async () => { + const db = await createPolicyTestClient( + ` + model User { + id String @id @default(uuid()) + m1 M1? + + @@allow('all', true) + } + + model M1 { + id String @id @default(uuid()) + value Int + user User? @relation(fields: [userId], references: [id]) + userId String? @unique + + @@allow('read,create,update', true) + @@allow('delete', auth().id == 'user1' && value > 0) + } + `, + ); + + await db.$setAuth({ id: 'user1' }).m1.create({ + data: { + id: 'm1', + value: 1, + }, + }); + + await expect( + db.$setAuth({ id: 'user2' }).user.create({ + data: { + id: 'user2', + m1: { + connect: { id: 'm1' }, + }, + }, + }), + ).toResolveTruthy(); + + await expect( + db.$setAuth({ id: 'user2' }).user.update({ + where: { id: 'user2' }, + data: { + m1: { delete: true }, + }, + }), + ).toBeRejectedNotFound(); + + await expect( + db.$setAuth({ id: 'user1' }).user.create({ + data: { + id: 'user1', + m1: { + connect: { id: 'm1' }, + }, + }, + }), + ).toResolveTruthy(); + + await expect( + db.$setAuth({ id: 'user1' }).user.update({ + where: { id: 'user1' }, + data: { + m1: { delete: true }, + }, + }), + ).toResolveTruthy(); + + expect(await db.$unuseAll().m1.findMany()).toHaveLength(0); + }); +}); diff --git a/packages/runtime/test/policy/migrated/petstore-sample.test.ts b/packages/runtime/test/policy/migrated/petstore-sample.test.ts new file mode 100644 index 00000000..99e5e8c7 --- /dev/null +++ b/packages/runtime/test/policy/migrated/petstore-sample.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest'; +import { createPolicyTestClient } from '../utils'; +import { schema } from '../../schemas/petstore/schema'; + +// TODO: `future()` support +describe.skip('Pet Store Policy Tests', () => { + it('crud', async () => { + const petData = [ + { + id: 'luna', + name: 'Luna', + category: 'kitten', + }, + { + id: 'max', + name: 'Max', + category: 'doggie', + }, + { + id: 'cooper', + name: 'Cooper', + category: 'reptile', + }, + ]; + + const db = await createPolicyTestClient(schema); + + for (const pet of petData) { + await db.$unuseAll().pet.create({ data: pet }); + } + + await db.$unuseAll().user.create({ data: { id: 'user1', email: 'user1@abc.com' } }); + + const r = await db.$setAuth({ id: 'user1' }).order.create({ + include: { user: true, pets: true }, + data: { + user: { connect: { id: 'user1' } }, + pets: { connect: [{ id: 'luna' }, { id: 'max' }] }, + }, + }); + + expect(r.user.id).toBe('user1'); + expect(r.pets).toHaveLength(2); + }); +}); diff --git a/packages/runtime/test/policy/migrated/query-reduction.test.ts b/packages/runtime/test/policy/migrated/query-reduction.test.ts new file mode 100644 index 00000000..61b11d06 --- /dev/null +++ b/packages/runtime/test/policy/migrated/query-reduction.test.ts @@ -0,0 +1,137 @@ +import { describe, expect, it } from 'vitest'; +import { createPolicyTestClient } from '../utils'; + +describe('With Policy: query reduction', () => { + it('test query reduction', async () => { + const db = await createPolicyTestClient( + ` + model User { + id Int @id @default(autoincrement()) + role String @default("User") + posts Post[] + private Boolean @default(false) + age Int + + @@allow('all', auth() == this) + @@allow('read', !private) + } + + model Post { + id Int @id @default(autoincrement()) + user User @relation(fields: [userId], references: [id]) + userId Int + title String + published Boolean @default(false) + viewCount Int @default(0) + + @@allow('all', auth() == user) + @@allow('read', published) + } + `, + ); + + await db.$unuseAll().user.create({ + data: { + id: 1, + role: 'User', + age: 18, + posts: { + create: [ + { id: 1, title: 'Post 1' }, + { id: 2, title: 'Post 2', published: true }, + ], + }, + }, + }); + await db.$unuseAll().user.create({ + data: { + id: 2, + role: 'Admin', + age: 28, + private: true, + posts: { + create: [{ id: 3, title: 'Post 3', viewCount: 100 }], + }, + }, + }); + + const dbUser1 = db.$setAuth({ id: 1 }); + const dbUser2 = db.$setAuth({ id: 2 }); + + await expect( + dbUser1.user.findMany({ + where: { id: 2, AND: { age: { gt: 20 } } }, + }), + ).resolves.toHaveLength(0); + + await expect( + dbUser2.user.findMany({ + where: { id: 2, AND: { age: { gt: 20 } } }, + }), + ).resolves.toHaveLength(1); + + await expect( + dbUser1.user.findMany({ + where: { + AND: { age: { gt: 10 } }, + OR: [{ age: { gt: 25 } }, { age: { lt: 20 } }], + NOT: { private: true }, + }, + }), + ).resolves.toHaveLength(1); + + await expect( + dbUser2.user.findMany({ + where: { + AND: { age: { gt: 10 } }, + OR: [{ age: { gt: 25 } }, { age: { lt: 20 } }], + NOT: { private: true }, + }, + }), + ).resolves.toHaveLength(1); + + // to-many relation query + await expect( + dbUser1.user.findMany({ + where: { posts: { some: { published: true } } }, + }), + ).resolves.toHaveLength(1); + await expect( + dbUser1.user.findMany({ + where: { posts: { some: { AND: [{ published: true }, { viewCount: { gt: 0 } }] } } }, + }), + ).resolves.toHaveLength(0); + await expect( + dbUser2.user.findMany({ + where: { posts: { some: { AND: [{ published: false }, { viewCount: { gt: 0 } }] } } }, + }), + ).resolves.toHaveLength(1); + await expect( + dbUser1.user.findMany({ + where: { posts: { every: { published: true } } }, + }), + ).resolves.toHaveLength(0); + await expect( + dbUser1.user.findMany({ + where: { posts: { none: { published: true } } }, + }), + ).resolves.toHaveLength(0); + + // to-one relation query + await expect( + dbUser1.post.findMany({ + where: { user: { role: 'Admin' } }, + }), + ).resolves.toHaveLength(0); + await expect( + dbUser1.post.findMany({ + where: { user: { is: { role: 'Admin' } } }, + }), + ).resolves.toHaveLength(0); + await expect( + dbUser1.post.findMany({ + where: { user: { isNot: { role: 'User' } } }, + }), + ).resolves.toHaveLength(0); + }); +}); diff --git a/packages/runtime/test/schemas/petstore/input.ts b/packages/runtime/test/schemas/petstore/input.ts new file mode 100644 index 00000000..6aece67e --- /dev/null +++ b/packages/runtime/test/schemas/petstore/input.ts @@ -0,0 +1,70 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaType as $Schema } from "./schema"; +import type { FindManyArgs as $FindManyArgs, FindUniqueArgs as $FindUniqueArgs, FindFirstArgs as $FindFirstArgs, CreateArgs as $CreateArgs, CreateManyArgs as $CreateManyArgs, CreateManyAndReturnArgs as $CreateManyAndReturnArgs, UpdateArgs as $UpdateArgs, UpdateManyArgs as $UpdateManyArgs, UpdateManyAndReturnArgs as $UpdateManyAndReturnArgs, UpsertArgs as $UpsertArgs, DeleteArgs as $DeleteArgs, DeleteManyArgs as $DeleteManyArgs, CountArgs as $CountArgs, AggregateArgs as $AggregateArgs, GroupByArgs as $GroupByArgs, WhereInput as $WhereInput, SelectInput as $SelectInput, IncludeInput as $IncludeInput, OmitInput as $OmitInput } from "@zenstackhq/runtime"; +import type { SimplifiedModelResult as $SimplifiedModelResult, SelectIncludeOmit as $SelectIncludeOmit } from "@zenstackhq/runtime"; +export type UserFindManyArgs = $FindManyArgs<$Schema, "User">; +export type UserFindUniqueArgs = $FindUniqueArgs<$Schema, "User">; +export type UserFindFirstArgs = $FindFirstArgs<$Schema, "User">; +export type UserCreateArgs = $CreateArgs<$Schema, "User">; +export type UserCreateManyArgs = $CreateManyArgs<$Schema, "User">; +export type UserCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "User">; +export type UserUpdateArgs = $UpdateArgs<$Schema, "User">; +export type UserUpdateManyArgs = $UpdateManyArgs<$Schema, "User">; +export type UserUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "User">; +export type UserUpsertArgs = $UpsertArgs<$Schema, "User">; +export type UserDeleteArgs = $DeleteArgs<$Schema, "User">; +export type UserDeleteManyArgs = $DeleteManyArgs<$Schema, "User">; +export type UserCountArgs = $CountArgs<$Schema, "User">; +export type UserAggregateArgs = $AggregateArgs<$Schema, "User">; +export type UserGroupByArgs = $GroupByArgs<$Schema, "User">; +export type UserWhereInput = $WhereInput<$Schema, "User">; +export type UserSelect = $SelectInput<$Schema, "User">; +export type UserInclude = $IncludeInput<$Schema, "User">; +export type UserOmit = $OmitInput<$Schema, "User">; +export type UserGetPayload> = $SimplifiedModelResult<$Schema, "User", Args>; +export type PetFindManyArgs = $FindManyArgs<$Schema, "Pet">; +export type PetFindUniqueArgs = $FindUniqueArgs<$Schema, "Pet">; +export type PetFindFirstArgs = $FindFirstArgs<$Schema, "Pet">; +export type PetCreateArgs = $CreateArgs<$Schema, "Pet">; +export type PetCreateManyArgs = $CreateManyArgs<$Schema, "Pet">; +export type PetCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Pet">; +export type PetUpdateArgs = $UpdateArgs<$Schema, "Pet">; +export type PetUpdateManyArgs = $UpdateManyArgs<$Schema, "Pet">; +export type PetUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Pet">; +export type PetUpsertArgs = $UpsertArgs<$Schema, "Pet">; +export type PetDeleteArgs = $DeleteArgs<$Schema, "Pet">; +export type PetDeleteManyArgs = $DeleteManyArgs<$Schema, "Pet">; +export type PetCountArgs = $CountArgs<$Schema, "Pet">; +export type PetAggregateArgs = $AggregateArgs<$Schema, "Pet">; +export type PetGroupByArgs = $GroupByArgs<$Schema, "Pet">; +export type PetWhereInput = $WhereInput<$Schema, "Pet">; +export type PetSelect = $SelectInput<$Schema, "Pet">; +export type PetInclude = $IncludeInput<$Schema, "Pet">; +export type PetOmit = $OmitInput<$Schema, "Pet">; +export type PetGetPayload> = $SimplifiedModelResult<$Schema, "Pet", Args>; +export type OrderFindManyArgs = $FindManyArgs<$Schema, "Order">; +export type OrderFindUniqueArgs = $FindUniqueArgs<$Schema, "Order">; +export type OrderFindFirstArgs = $FindFirstArgs<$Schema, "Order">; +export type OrderCreateArgs = $CreateArgs<$Schema, "Order">; +export type OrderCreateManyArgs = $CreateManyArgs<$Schema, "Order">; +export type OrderCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Order">; +export type OrderUpdateArgs = $UpdateArgs<$Schema, "Order">; +export type OrderUpdateManyArgs = $UpdateManyArgs<$Schema, "Order">; +export type OrderUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Order">; +export type OrderUpsertArgs = $UpsertArgs<$Schema, "Order">; +export type OrderDeleteArgs = $DeleteArgs<$Schema, "Order">; +export type OrderDeleteManyArgs = $DeleteManyArgs<$Schema, "Order">; +export type OrderCountArgs = $CountArgs<$Schema, "Order">; +export type OrderAggregateArgs = $AggregateArgs<$Schema, "Order">; +export type OrderGroupByArgs = $GroupByArgs<$Schema, "Order">; +export type OrderWhereInput = $WhereInput<$Schema, "Order">; +export type OrderSelect = $SelectInput<$Schema, "Order">; +export type OrderInclude = $IncludeInput<$Schema, "Order">; +export type OrderOmit = $OmitInput<$Schema, "Order">; +export type OrderGetPayload> = $SimplifiedModelResult<$Schema, "Order", Args>; diff --git a/packages/runtime/test/schemas/petstore/models.ts b/packages/runtime/test/schemas/petstore/models.ts new file mode 100644 index 00000000..6526b66c --- /dev/null +++ b/packages/runtime/test/schemas/petstore/models.ts @@ -0,0 +1,12 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaType as $Schema } from "./schema"; +import { type ModelResult as $ModelResult } from "@zenstackhq/runtime"; +export type User = $ModelResult<$Schema, "User">; +export type Pet = $ModelResult<$Schema, "Pet">; +export type Order = $ModelResult<$Schema, "Order">; diff --git a/packages/runtime/test/schemas/petstore/schema.ts b/packages/runtime/test/schemas/petstore/schema.ts new file mode 100644 index 00000000..c6902c7e --- /dev/null +++ b/packages/runtime/test/schemas/petstore/schema.ts @@ -0,0 +1,156 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaDef, ExpressionUtils } from "../../../dist/schema"; +export const schema = { + provider: { + type: "sqlite" + }, + models: { + User: { + name: "User", + fields: { + id: { + name: "id", + type: "String", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("cuid") }] }], + default: ExpressionUtils.call("cuid") + }, + email: { + name: "email", + type: "String", + unique: true, + attributes: [{ name: "@unique" }] + }, + orders: { + name: "orders", + type: "Order", + array: true, + relation: { opposite: "user" } + } + }, + attributes: [ + { name: "@@allow", args: [{ name: "operation", value: ExpressionUtils.literal("create") }, { name: "condition", value: ExpressionUtils.literal(true) }] }, + { name: "@@allow", args: [{ name: "operation", value: ExpressionUtils.literal("read") }, { name: "condition", value: ExpressionUtils.literal(true) }] } + ], + idFields: ["id"], + uniqueFields: { + id: { type: "String" }, + email: { type: "String" } + } + }, + Pet: { + name: "Pet", + fields: { + id: { + name: "id", + type: "String", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("cuid") }] }], + default: ExpressionUtils.call("cuid") + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }], + default: ExpressionUtils.call("now") + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + attributes: [{ name: "@updatedAt" }] + }, + name: { + name: "name", + type: "String" + }, + category: { + name: "category", + type: "String" + }, + order: { + name: "order", + type: "Order", + optional: true, + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("orderId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + relation: { opposite: "pets", fields: ["orderId"], references: ["id"] } + }, + orderId: { + name: "orderId", + type: "String", + optional: true, + foreignKeyFor: [ + "order" + ] + } + }, + attributes: [ + { name: "@@allow", args: [{ name: "operation", value: ExpressionUtils.literal("read") }, { name: "condition", value: ExpressionUtils.binary(ExpressionUtils.binary(ExpressionUtils.field("orderId"), "==", ExpressionUtils._null()), "||", ExpressionUtils.binary(ExpressionUtils.member(ExpressionUtils.field("order"), ["user"]), "==", ExpressionUtils.call("auth"))) }] }, + { name: "@@allow", args: [{ name: "operation", value: ExpressionUtils.literal("update") }, { name: "condition", value: ExpressionUtils.binary(ExpressionUtils.binary(ExpressionUtils.binary(ExpressionUtils.field("name"), "==", ExpressionUtils.member(ExpressionUtils.call("future"), ["name"])), "&&", ExpressionUtils.binary(ExpressionUtils.field("category"), "==", ExpressionUtils.member(ExpressionUtils.call("future"), ["category"]))), "&&", ExpressionUtils.binary(ExpressionUtils.field("orderId"), "==", ExpressionUtils._null())) }] } + ], + idFields: ["id"], + uniqueFields: { + id: { type: "String" } + } + }, + Order: { + name: "Order", + fields: { + id: { + name: "id", + type: "String", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("cuid") }] }], + default: ExpressionUtils.call("cuid") + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }], + default: ExpressionUtils.call("now") + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + attributes: [{ name: "@updatedAt" }] + }, + pets: { + name: "pets", + type: "Pet", + array: true, + relation: { opposite: "order" } + }, + user: { + name: "user", + type: "User", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + relation: { opposite: "orders", fields: ["userId"], references: ["id"] } + }, + userId: { + name: "userId", + type: "String", + foreignKeyFor: [ + "user" + ] + } + }, + attributes: [ + { name: "@@allow", args: [{ name: "operation", value: ExpressionUtils.literal("read,create") }, { name: "condition", value: ExpressionUtils.binary(ExpressionUtils.call("auth"), "==", ExpressionUtils.field("user")) }] } + ], + idFields: ["id"], + uniqueFields: { + id: { type: "String" } + } + } + }, + authType: "User", + plugins: {} +} as const satisfies SchemaDef; +export type SchemaType = typeof schema; diff --git a/packages/runtime/test/schemas/petstore/schema.zmodel b/packages/runtime/test/schemas/petstore/schema.zmodel new file mode 100644 index 00000000..4a2442ca --- /dev/null +++ b/packages/runtime/test/schemas/petstore/schema.zmodel @@ -0,0 +1,52 @@ +datasource db { + provider = 'sqlite' + url = 'file:./petstore.db' +} + +generator js { + provider = 'prisma-client-js' +} + +plugin zod { + provider = '@core/zod' +} + +model User { + id String @id @default(cuid()) + email String @unique + orders Order[] + + // everybody can signup + @@allow('create', true) + + // user profile is publicly readable + @@allow('read', true) +} + +model Pet { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + name String + category String + order Order? @relation(fields: [orderId], references: [id]) + orderId String? + + // unsold pets are readable to all; sold ones are readable to buyers only + @@allow('read', orderId == null || order.user == auth()) + + // only allow update to 'orderId' field if it's not set yet (unsold) + @@allow('update', name == future().name && category == future().category && orderId == null ) +} + +model Order { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + pets Pet[] + user User @relation(fields: [userId], references: [id]) + userId String + + // users can read their orders + @@allow('read,create', auth() == user) +}