Skip to content
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
1 change: 1 addition & 0 deletions BREAKINGCHANGES.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
1. `auth()` cannot be directly compared with a relation anymore
2. `update` and `delete` policy rejection throws `NotFoundError`
3. non-optional to-one relation doesn't automatically filter parent read when evaluating access policies
44 changes: 31 additions & 13 deletions packages/runtime/src/client/crud/dialects/sqlite.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type Decimal from 'decimal.js';
import {
ExpressionWrapper,
sql,
Expand All @@ -17,7 +18,6 @@ import {
requireModel,
} from '../../query-utils';
import { BaseCrudDialect } from './base';
import type Decimal from 'decimal.js';

export class SqliteCrudDialect<
Schema extends SchemaDef
Expand Down Expand Up @@ -123,11 +123,11 @@ export class SqliteCrudDialect<
}

tbl = tbl.select(() => {
const objArgs: Array<
type ArgsType =
| Expression<any>
| RawBuilder<any>
| SelectQueryBuilder<any, any, {}>
> = [];
| SelectQueryBuilder<any, any, {}>;
const objArgs: ArgsType[] = [];

if (payload === true || !payload.select) {
// select all scalar fields
Expand Down Expand Up @@ -156,18 +156,36 @@ export class SqliteCrudDialect<
} else if (payload.select) {
// select specific fields
objArgs.push(
...Object.entries(payload.select)
...Object.entries<any>(payload.select)
.filter(([, value]) => value)
.map(([field]) => [
sql.lit(field),
buildFieldRef(
.map(([field, value]) => {
const fieldDef = requireField(
this.schema,
relationModel,
field,
this.options,
eb
),
])
field
);
if (fieldDef.relation) {
const subJson = this.buildRelationJSON(
relationModel as GetModels<Schema>,
eb,
field,
`${parentName}$${relationField}`,
value
);
return [sql.lit(field), subJson as ArgsType];
} else {
return [
sql.lit(field),
buildFieldRef(
this.schema,
relationModel,
field,
this.options,
eb
) as ArgsType,
];
}
})
.flatMap((v) => v)
);
}
Expand Down
1 change: 1 addition & 0 deletions packages/runtime/src/client/crud/operations/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export class CreateOperationHandler<

if (!result) {
throw new RejectedByPolicyError(
this.model,
`result is not allowed to be read back`
);
}
Expand Down
1 change: 1 addition & 0 deletions packages/runtime/src/client/crud/operations/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export class UpdateOperationHandler<

if (!result) {
throw new RejectedByPolicyError(
this.model,
'result is not allowed to be read back'
);
}
Expand Down
24 changes: 15 additions & 9 deletions packages/runtime/src/client/executor/zenstack-query-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import type { PromiseType } from 'utility-types';
import type { GetModels, SchemaDef } from '../../schema';
import type { ClientImpl } from '../client-impl';
import type { ClientContract } from '../contract';
import { InternalError } from '../errors';
import { InternalError, QueryError } from '../errors';
import type {
MutationInterceptionFilterResult,
OnKyselyQueryTransactionCallback,
Expand Down Expand Up @@ -158,17 +158,23 @@ export class ZenStackQueryExecutor<
return proceed(queryNode);
}

private proceedQuery(query: RootOperationNode, queryId: QueryId) {
private async proceedQuery(query: RootOperationNode, queryId: QueryId) {
// run built-in transformers
const finalQuery = this.nameMapper.transformNode(query);
const compiled = this.compileQuery(finalQuery);
return this.driver.txConnection
? super
.withConnectionProvider(
new SingleConnectionProvider(this.driver.txConnection)
)
.executeQuery<any>(compiled, queryId)
: super.executeQuery<any>(compiled, queryId);
try {
return this.driver.txConnection
? await super
.withConnectionProvider(
new SingleConnectionProvider(this.driver.txConnection)
)
.executeQuery<any>(compiled, queryId)
: await super.executeQuery<any>(compiled, queryId);
} catch (err) {
throw new QueryError(
`Policy: failed to execute query: ${err}, sql: ${compiled.sql}, parameters: ${compiled.parameters}`
);
}
}

private isMutationNode(queryNode: RootOperationNode) {
Expand Down
9 changes: 7 additions & 2 deletions packages/runtime/src/plugins/policy/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
* Error thrown when an operation is rejected by access policy.
*/
export class RejectedByPolicyError extends Error {
constructor(reason?: string) {
super(reason ?? `Operation rejected by policy`);
constructor(
public readonly model: string | undefined,
public readonly reason?: string
) {
super(
reason ?? `Operation rejected by policy${model ? ': ' + model : ''}`
);
}
}
Loading