Skip to content

Commit 9ac70f7

Browse files
committed
Merge remote-tracking branch 'origin/dev' into feat/types
2 parents 041e0a9 + 3b18362 commit 9ac70f7

File tree

12 files changed

+519
-78
lines changed

12 files changed

+519
-78
lines changed

packages/plugins/swr/src/runtime/index.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
type ModelMeta,
88
type PrismaWriteActionType,
99
} from '@zenstackhq/runtime/cross';
10-
import * as crossFetch from 'cross-fetch';
1110
import { lowerCaseFirst } from 'lower-case-first';
1211
import { createContext, useContext } from 'react';
1312
import type { Cache, Fetcher, SWRConfiguration, SWRResponse } from 'swr';
@@ -376,10 +375,19 @@ export function useInvalidation(model: string, modelMeta: ModelMeta): Invalidato
376375
export async function fetcher<R, C extends boolean>(
377376
url: string,
378377
options?: RequestInit,
379-
fetch?: FetchFn,
378+
customFetch?: FetchFn,
380379
checkReadBack?: C
381380
): Promise<C extends true ? R | undefined : R> {
382-
const _fetch = fetch ?? crossFetch.fetch;
381+
// Note: 'cross-fetch' is supposed to handle fetch compatibility
382+
// but it doesn't work for cloudflare workers
383+
const _fetch =
384+
customFetch ??
385+
// check if fetch is available globally
386+
(typeof fetch === 'function'
387+
? fetch
388+
: // fallback to 'cross-fetch' if otherwise
389+
(await import('cross-fetch')).default);
390+
383391
const res = await _fetch(url, options);
384392
if (!res.ok) {
385393
const errData = unmarshal(await res.text());
@@ -420,7 +428,7 @@ function marshal(value: unknown) {
420428

421429
function unmarshal(value: string) {
422430
const parsed = JSON.parse(value);
423-
if (parsed.data && parsed.meta?.serialization) {
431+
if (typeof parsed === 'object' && parsed?.data && parsed?.meta?.serialization) {
424432
const deserializedData = deserialize(parsed.data, parsed.meta.serialization);
425433
return { ...parsed, data: deserializedData };
426434
} else {

packages/plugins/tanstack-query/src/runtime/common.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
type ModelMeta,
99
type PrismaWriteActionType,
1010
} from '@zenstackhq/runtime/cross';
11-
import * as crossFetch from 'cross-fetch';
1211

1312
/**
1413
* The default query endpoint.
@@ -133,10 +132,19 @@ export type APIContext = {
133132
export async function fetcher<R, C extends boolean>(
134133
url: string,
135134
options?: RequestInit,
136-
fetch?: FetchFn,
135+
customFetch?: FetchFn,
137136
checkReadBack?: C
138137
): Promise<C extends true ? R | undefined : R> {
139-
const _fetch = fetch ?? crossFetch.fetch;
138+
// Note: 'cross-fetch' is supposed to handle fetch compatibility
139+
// but it doesn't work for cloudflare workers
140+
const _fetch =
141+
customFetch ??
142+
// check if fetch is available globally
143+
(typeof fetch === 'function'
144+
? fetch
145+
: // fallback to 'cross-fetch' if otherwise
146+
(await import('cross-fetch')).default);
147+
140148
const res = await _fetch(url, options);
141149
if (!res.ok) {
142150
const errData = unmarshal(await res.text());
@@ -213,7 +221,7 @@ export function marshal(value: unknown) {
213221

214222
export function unmarshal(value: string) {
215223
const parsed = JSON.parse(value);
216-
if (parsed.data && parsed.meta?.serialization) {
224+
if (typeof parsed === 'object' && parsed?.data && parsed?.meta?.serialization) {
217225
const deserializedData = deserialize(parsed.data, parsed.meta.serialization);
218226
return { ...parsed, data: deserializedData };
219227
} else {

packages/runtime/src/enhancements/node/delegate.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
9393

9494
if (args.orderBy) {
9595
// `orderBy` may contain fields from base types
96-
this.injectWhereHierarchy(this.model, args.orderBy);
96+
enumerate(args.orderBy).forEach((item) => this.injectWhereHierarchy(model, item));
9797
}
9898

9999
if (this.options.logPrismaQuery) {
@@ -206,7 +206,9 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
206206
if (fieldValue !== undefined) {
207207
if (fieldValue.orderBy) {
208208
// `orderBy` may contain fields from base types
209-
this.injectWhereHierarchy(fieldInfo.type, fieldValue.orderBy);
209+
enumerate(fieldValue.orderBy).forEach((item) =>
210+
this.injectWhereHierarchy(fieldInfo.type, item)
211+
);
210212
}
211213

212214
if (this.injectBaseFieldSelect(model, field, fieldValue, args, kind)) {
@@ -1037,7 +1039,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
10371039
}
10381040

10391041
if (args.orderBy) {
1040-
this.injectWhereHierarchy(this.model, args.orderBy);
1042+
enumerate(args.orderBy).forEach((item) => this.injectWhereHierarchy(this.model, item));
10411043
}
10421044

10431045
if (args.where) {

packages/runtime/src/enhancements/node/policy/policy-utils.ts

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import deepmerge from 'deepmerge';
44
import { isPlainObject } from 'is-plain-object';
55
import { lowerCaseFirst } from 'lower-case-first';
6+
import traverse from 'traverse';
67
import { upperCaseFirst } from 'upper-case-first';
78
import { z, type ZodError, type ZodObject, type ZodSchema } from 'zod';
89
import { fromZodError } from 'zod-validation-error';
@@ -31,7 +32,15 @@ import { getVersion } from '../../../version';
3132
import type { InternalEnhancementOptions } from '../create-enhancement';
3233
import { Logger } from '../logger';
3334
import { QueryUtils } from '../query-utils';
34-
import type { EntityChecker, ModelPolicyDef, PermissionCheckerFunc, PolicyDef, PolicyFunc } from '../types';
35+
import type {
36+
DelegateConstraint,
37+
EntityChecker,
38+
ModelPolicyDef,
39+
PermissionCheckerFunc,
40+
PolicyDef,
41+
PolicyFunc,
42+
VariableConstraint,
43+
} from '../types';
3544
import { formatObject, prismaClientKnownRequestError } from '../utils';
3645

3746
/**
@@ -667,7 +676,47 @@ export class PolicyUtil extends QueryUtils {
667676
}
668677

669678
// call checker function
670-
return checker({ user: this.user });
679+
let result = checker({ user: this.user });
680+
681+
// the constraint may contain "delegate" ones that should be resolved
682+
// by evaluating the corresponding checker of the delegated models
683+
684+
const isVariableConstraint = (value: any): value is VariableConstraint => {
685+
return value && typeof value === 'object' && value.kind === 'variable';
686+
};
687+
688+
const isDelegateConstraint = (value: any): value is DelegateConstraint => {
689+
return value && typeof value === 'object' && value.kind === 'delegate';
690+
};
691+
692+
// here we prefix the constraint variables coming from delegated checkers
693+
// with the relation field name to avoid conflicts
694+
const prefixConstraintVariables = (constraint: unknown, prefix: string) => {
695+
return traverse(constraint).map(function (value) {
696+
if (isVariableConstraint(value)) {
697+
this.update(
698+
{
699+
...value,
700+
name: `${prefix}${value.name}`,
701+
},
702+
true
703+
);
704+
}
705+
});
706+
};
707+
708+
// eslint-disable-next-line @typescript-eslint/no-this-alias
709+
const that = this;
710+
result = traverse(result).forEach(function (value) {
711+
if (isDelegateConstraint(value)) {
712+
const { model: delegateModel, relation, operation: delegateOp } = value;
713+
let newValue = that.getCheckerConstraint(delegateModel, delegateOp ?? operation);
714+
newValue = prefixConstraintVariables(newValue, `${relation}.`);
715+
this.update(newValue, true);
716+
}
717+
});
718+
719+
return result;
671720
}
672721

673722
//#endregion

packages/runtime/src/enhancements/node/types.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ export interface CommonEnhancementOptions {
1818
prismaModule?: any;
1919
}
2020

21+
/**
22+
* CRUD operations
23+
*/
24+
export type CRUD = 'create' | 'read' | 'update' | 'delete';
25+
2126
/**
2227
* Function for getting policy guard with a given context
2328
*/
@@ -74,14 +79,26 @@ export type LogicalConstraint = {
7479
children: PermissionCheckerConstraint[];
7580
};
7681

82+
/**
83+
* Constraint delegated to another model through `check()` function call
84+
* on a relation field.
85+
*/
86+
export type DelegateConstraint = {
87+
kind: 'delegate';
88+
model: string;
89+
relation: string;
90+
operation?: CRUD;
91+
};
92+
7793
/**
7894
* Operation allowability checking constraint
7995
*/
8096
export type PermissionCheckerConstraint =
8197
| ValueConstraint
8298
| VariableConstraint
8399
| ComparisonConstraint
84-
| LogicalConstraint;
100+
| LogicalConstraint
101+
| DelegateConstraint;
85102

86103
/**
87104
* Policy definition

packages/schema/src/cli/actions/repl.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,18 @@ import { inspect } from 'util';
99
/**
1010
* CLI action for starting a REPL session
1111
*/
12-
export async function repl(projectPath: string, options: { prismaClient?: string; debug?: boolean; table?: boolean }) {
12+
export async function repl(
13+
projectPath: string,
14+
options: { loadPath?: string; prismaClient?: string; debug?: boolean; table?: boolean }
15+
) {
1316
if (!process?.stdout?.isTTY && process?.versions?.bun) {
14-
console.error('REPL on Bun is only available in a TTY terminal at this time. Please use npm/npx to run the command in this context instead of bun/bunx.');
17+
console.error(
18+
'REPL on Bun is only available in a TTY terminal at this time. Please use npm/npx to run the command in this context instead of bun/bunx.'
19+
);
1520
return;
1621
}
1722

18-
const prettyRepl = await import('pretty-repl')
23+
const prettyRepl = await import('pretty-repl');
1924

2025
console.log('Welcome to ZenStack REPL. See help with the ".help" command.');
2126
console.log('Global variables:');
@@ -47,7 +52,9 @@ export async function repl(projectPath: string, options: { prismaClient?: string
4752
}
4853
}
4954

50-
const { enhance } = require('@zenstackhq/runtime');
55+
const { enhance } = options.loadPath
56+
? require(path.join(path.resolve(options.loadPath), 'enhance'))
57+
: require('@zenstackhq/runtime');
5158

5259
let debug = !!options.debug;
5360
let table = !!options.table;
@@ -63,7 +70,11 @@ export async function repl(projectPath: string, options: { prismaClient?: string
6370
let r: any = undefined;
6471
let isPrismaCall = false;
6572

66-
if (cmd.includes('await ')) {
73+
if (/^\s*user\s*=[^=]/.test(cmd)) {
74+
// assigning to user variable, reset auth
75+
eval(cmd);
76+
setAuth(user);
77+
} else if (/^\s*await\s+/.test(cmd)) {
6778
// eval can't handle top-level await, so we wrap it in an async function
6879
cmd = `(async () => (${cmd}))()`;
6980
r = eval(cmd);
@@ -137,14 +148,18 @@ export async function repl(projectPath: string, options: { prismaClient?: string
137148

138149
// .auth command
139150
replServer.defineCommand('auth', {
140-
help: 'Set current user. Run without argument to switch to anonymous. Pass an user object to set current user.',
151+
help: 'Set current user. Run without argument to switch to anonymous. Pass an user object to set current user. Run ".auth info" to show current user.',
141152
action(value: string) {
142153
this.clearBufferedCommand();
143154
try {
144155
if (!value?.trim()) {
145156
// set anonymous
146157
setAuth(undefined);
147158
console.log(`Auth user: anonymous. Use ".auth { id: ... }" to change.`);
159+
} else if (value.trim() === 'info') {
160+
// refresh auth user
161+
setAuth(user);
162+
console.log(`Current user: ${user ? inspect(user) : 'anonymous'}`);
148163
} else {
149164
// set current user
150165
const user = eval(`(${value})`);

packages/schema/src/cli/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ export function createProgram() {
133133
program
134134
.command('repl')
135135
.description('Start a REPL session.')
136-
.option('--prisma-client <module>', 'path to Prisma client module')
136+
.option('--load-path <path>', 'path to load modules generated by ZenStack')
137+
.option('--prisma-client <path>', 'path to Prisma client module')
137138
.option('--debug', 'enable debug output')
138139
.option('--table', 'enable table format output')
139140
.action(replAction);

packages/schema/src/plugins/enhancer/policy/constraint-transformer.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import {
2+
PluginError,
3+
getLiteral,
24
getRelationKeyPairs,
35
isAuthInvocation,
46
isDataModelFieldReference,
@@ -7,9 +9,11 @@ import {
79
import {
810
BinaryExpr,
911
BooleanLiteral,
12+
DataModel,
1013
DataModelField,
1114
Expression,
1215
ExpressionType,
16+
InvocationExpr,
1317
LiteralExpr,
1418
MemberAccessExpr,
1519
NumberLiteral,
@@ -27,6 +31,8 @@ import {
2731
isUnaryExpr,
2832
} from '@zenstackhq/sdk/ast';
2933
import { P, match } from 'ts-pattern';
34+
import { name } from '..';
35+
import { isCheckInvocation } from '../../../utils/ast-utils';
3036

3137
/**
3238
* Options for {@link ConstraintTransformer}.
@@ -107,6 +113,8 @@ export class ConstraintTransformer {
107113
.when(isReferenceExpr, (expr) => this.transformReference(expr))
108114
// top-level boolean member access expr
109115
.when(isMemberAccessExpr, (expr) => this.transformMemberAccess(expr))
116+
// `check()` invocation on a relation field
117+
.when(isCheckInvocation, (expr) => this.transformCheckInvocation(expr as InvocationExpr))
110118
.otherwise(() => this.nextVar())
111119
);
112120
}
@@ -259,6 +267,30 @@ export class ConstraintTransformer {
259267
return undefined;
260268
}
261269

270+
private transformCheckInvocation(expr: InvocationExpr) {
271+
// transform `check()` invocation to a special "delegate" constraint kind
272+
// to be evaluated at runtime
273+
274+
const field = expr.args[0].value as ReferenceExpr;
275+
if (!field) {
276+
throw new PluginError(name, 'Invalid check invocation');
277+
}
278+
const fieldType = field.$resolvedType?.decl as DataModel;
279+
280+
let operation: string | undefined = undefined;
281+
if (expr.args[1]) {
282+
operation = getLiteral<string>(expr.args[1].value);
283+
}
284+
285+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
286+
const result: any = { kind: 'delegate', model: fieldType.name, relation: field.target.$refText };
287+
if (operation) {
288+
// operation can be explicitly specified or inferred from the context
289+
result.operation = operation;
290+
}
291+
return JSON.stringify(result);
292+
}
293+
262294
// normalize `auth()` access undefined value to null
263295
private normalizeToNull(expr: string) {
264296
return `(${expr} ?? null)`;

0 commit comments

Comments
 (0)