Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</a>
</div>

> V3 is currently in alpha phase and not ready for production use. Feedback and bug reports are greatly appreciated. Please visit this dedicated [discord channel](https://discord.com/channels/1035538056146595961/1352359627525718056) for chat and support.
> V3 is currently in beta phase and not ready for production use. Feedback and bug reports are greatly appreciated. Please visit this dedicated [discord channel](https://discord.com/channels/1035538056146595961/1352359627525718056) for chat and support.

# What's ZenStack

Expand Down
33 changes: 25 additions & 8 deletions packages/runtime/src/client/crud/operations/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
);
Object.assign(createFields, parentFkFields);
} else {
parentUpdateTask = (entity) => {
parentUpdateTask = async (entity) => {
const query = kysely
.updateTable(fromRelation.model)
.set(
Expand All @@ -300,7 +300,10 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
operation: 'update',
}),
);
return this.executeQuery(kysely, query, 'update');
const result = await this.executeQuery(kysely, query, 'update');
if (!result.numAffectedRows) {
throw new NotFoundError(fromRelation.model);
}
};
}
}
Expand Down Expand Up @@ -1551,8 +1554,11 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
fromRelation.field,
);
let updateResult: QueryResult<unknown>;
let updateModel: GetModels<Schema>;

if (ownedByModel) {
updateModel = fromRelation.model;

// set parent fk directly
invariant(_data.length === 1, 'only one entity can be connected');
const target = await this.readUnique(kysely, model, {
Expand Down Expand Up @@ -1581,6 +1587,8 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
);
updateResult = await this.executeQuery(kysely, query, 'connect');
} else {
updateModel = model;

// disconnect current if it's a one-one relation
const relationFieldDef = this.requireField(fromRelation.model, fromRelation.field);

Expand Down Expand Up @@ -1621,9 +1629,9 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
}

// validate connect result
if (_data.length > updateResult.numAffectedRows!) {
if (!updateResult.numAffectedRows || _data.length > updateResult.numAffectedRows) {
// some entities were not connected
throw new NotFoundError(model);
throw new NotFoundError(updateModel);
}
}
}
Expand Down Expand Up @@ -1735,7 +1743,10 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
operation: 'update',
}),
);
await this.executeQuery(kysely, query, 'disconnect');
const result = await this.executeQuery(kysely, query, 'disconnect');
if (!result.numAffectedRows) {
throw new NotFoundError(fromRelation.model);
}
} else {
// disconnect
const query = kysely
Expand Down Expand Up @@ -1859,7 +1870,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
const r = await this.executeQuery(kysely, query, 'connect');

// validate result
if (_data.length > r.numAffectedRows!) {
if (!r.numAffectedRows || _data.length > r.numAffectedRows) {
// some entities were not connected
throw new NotFoundError(model);
}
Expand Down Expand Up @@ -1892,9 +1903,12 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
}

let deleteResult: { count: number };
let deleteFromModel: GetModels<Schema>;
const m2m = getManyToManyRelation(this.schema, fromRelation.model, fromRelation.field);

if (m2m) {
deleteFromModel = model;

// handle many-to-many relation
const fieldDef = this.requireField(fromRelation.model, fromRelation.field);
invariant(fieldDef.relation?.opposite);
Expand All @@ -1919,11 +1933,13 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
);

if (ownedByModel) {
deleteFromModel = fromRelation.model;

const fromEntity = await this.readUnique(kysely, fromRelation.model as GetModels<Schema>, {
where: fromRelation.ids,
});
if (!fromEntity) {
throw new NotFoundError(model);
throw new NotFoundError(fromRelation.model);
}

const fieldDef = this.requireField(fromRelation.model, fromRelation.field);
Expand All @@ -1938,6 +1954,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
],
});
} else {
deleteFromModel = model;
deleteResult = await this.delete(kysely, model, {
AND: [
Object.fromEntries(keyPairs.map(({ fk, pk }) => [fk, fromRelation.ids[pk]])),
Expand All @@ -1952,7 +1969,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
// validate result
if (throwForNotFound && expectedDeleteCount > deleteResult.count) {
// some entities were not deleted
throw new NotFoundError(model);
throw new NotFoundError(deleteFromModel);
}
}

Expand Down
67 changes: 67 additions & 0 deletions packages/runtime/test/policy/crud/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,4 +206,71 @@ model Profile {

await expect(db.$setAuth({ id: 4 }).profile.create({ data: { id: 2, userId: 4 } })).toBeRejectedByPolicy();
});

it('works with nested create owner side', async () => {
const db = await createPolicyTestClient(
`
model User {
id Int @id
profile Profile?
@@allow('all', true)
}

model Profile {
id Int @id
user User? @relation(fields: [userId], references: [id])
userId Int? @unique

@@deny('all', auth() == null)
@@allow('create', user.id == auth().id)
@@allow('read', true)
}
`,
);

await expect(db.user.create({ data: { id: 1, profile: { create: { id: 1 } } } })).toBeRejectedByPolicy();
await expect(
db
.$setAuth({ id: 1 })
.user.create({ data: { id: 1, profile: { create: { id: 1 } } }, include: { profile: true } }),
).resolves.toMatchObject({
id: 1,
profile: {
id: 1,
},
});
});

it('works with nested create non-owner side', async () => {
const db = await createPolicyTestClient(
`
model User {
id Int @id
profile Profile?
@@deny('all', auth() == null)
@@allow('create', this.id == auth().id)
@@allow('read', true)
}

model Profile {
id Int @id
user User? @relation(fields: [userId], references: [id])
userId Int? @unique
@@allow('all', true)
}
`,
);

await expect(db.profile.create({ data: { id: 1, user: { create: { id: 1 } } } })).toBeRejectedByPolicy();
await expect(
db
.$setAuth({ id: 1 })
.profile.create({ data: { id: 1, user: { create: { id: 1 } } }, include: { user: true } }),
).resolves.toMatchObject({
id: 1,
user: {
id: 1,
},
});
});
});
Loading
Loading