Skip to content

Commit 32c258c

Browse files
authored
feat(enhance): Prisma Pulse support (#1658)
1 parent 40ea9fa commit 32c258c

File tree

8 files changed

+534
-308
lines changed

8 files changed

+534
-308
lines changed

packages/runtime/src/enhancements/omit.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,39 @@ class OmitHandler extends DefaultPrismaProxyHandler {
2626
}
2727

2828
// base override
29-
protected async processResultEntity<T>(data: T): Promise<T> {
30-
if (data) {
29+
protected async processResultEntity<T>(method: string, data: T): Promise<T> {
30+
if (!data || typeof data !== 'object') {
31+
return data;
32+
}
33+
34+
if (method === 'subscribe' || method === 'stream') {
35+
if (!('action' in data)) {
36+
return data;
37+
}
38+
39+
// Prisma Pulse result
40+
switch (data.action) {
41+
case 'create':
42+
if ('created' in data) {
43+
await this.doPostProcess(data.created, this.model);
44+
}
45+
break;
46+
case 'update':
47+
if ('before' in data) {
48+
await this.doPostProcess(data.before, this.model);
49+
}
50+
if ('after' in data) {
51+
await this.doPostProcess(data.after, this.model);
52+
}
53+
break;
54+
case 'delete':
55+
if ('deleted' in data) {
56+
await this.doPostProcess(data.deleted, this.model);
57+
}
58+
break;
59+
}
60+
} else {
61+
// regular prisma client result
3162
for (const value of enumerate(data)) {
3263
await this.doPostProcess(value, this.model);
3364
}

packages/runtime/src/enhancements/policy/handler.ts

Lines changed: 87 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1537,53 +1537,102 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
15371537

15381538
//#endregion
15391539

1540-
//#region Subscribe (Prisma Pulse)
1540+
//#region Prisma Pulse
15411541

15421542
subscribe(args: any) {
1543-
return createDeferredPromise(() => {
1544-
const readGuard = this.policyUtils.getAuthGuard(this.prisma, this.model, 'read');
1545-
if (this.policyUtils.isTrue(readGuard)) {
1546-
// no need to inject
1547-
if (this.shouldLogQuery) {
1548-
this.logger.info(`[policy] \`subscribe\` ${this.model}:\n${formatObject(args)}`);
1549-
}
1550-
return this.modelClient.subscribe(args);
1551-
}
1552-
1553-
if (!args) {
1554-
// include all
1555-
args = { create: {}, update: {}, delete: {} };
1556-
} else {
1557-
if (typeof args !== 'object') {
1558-
throw prismaClientValidationError(this.prisma, this.prismaModule, 'argument must be an object');
1559-
}
1560-
if (Object.keys(args).length === 0) {
1561-
// include all
1562-
args = { create: {}, update: {}, delete: {} };
1563-
} else {
1564-
args = this.policyUtils.safeClone(args);
1565-
}
1566-
}
1543+
return this.handleSubscribeStream('subscribe', args);
1544+
}
15671545

1568-
// inject into subscribe conditions
1546+
stream(args: any) {
1547+
return this.handleSubscribeStream('stream', args);
1548+
}
15691549

1570-
if (args.create) {
1571-
args.create.after = this.policyUtils.and(args.create.after, readGuard);
1550+
private async handleSubscribeStream(action: 'subscribe' | 'stream', args: any) {
1551+
if (!args) {
1552+
// include all
1553+
args = { create: {}, update: {}, delete: {} };
1554+
} else {
1555+
if (typeof args !== 'object') {
1556+
throw prismaClientValidationError(this.prisma, this.prismaModule, 'argument must be an object');
15721557
}
1558+
args = this.policyUtils.safeClone(args);
1559+
}
15731560

1574-
if (args.update) {
1575-
args.update.after = this.policyUtils.and(args.update.after, readGuard);
1561+
// inject read guard as subscription filter
1562+
for (const key of ['create', 'update', 'delete']) {
1563+
if (args[key] === undefined) {
1564+
continue;
15761565
}
1577-
1578-
if (args.delete) {
1579-
args.delete.before = this.policyUtils.and(args.delete.before, readGuard);
1566+
// "update" has an extra layer of "after"
1567+
const payload = key === 'update' ? args[key].after : args[key];
1568+
const toInject = { where: payload };
1569+
this.policyUtils.injectForRead(this.prisma, this.model, toInject);
1570+
if (key === 'update') {
1571+
// "update" has an extra layer of "after"
1572+
args[key].after = toInject.where;
1573+
} else {
1574+
args[key] = toInject.where;
15801575
}
1576+
}
15811577

1582-
if (this.shouldLogQuery) {
1583-
this.logger.info(`[policy] \`subscribe\` ${this.model}:\n${formatObject(args)}`);
1584-
}
1585-
return this.modelClient.subscribe(args);
1586-
});
1578+
if (this.shouldLogQuery) {
1579+
this.logger.info(`[policy] \`${action}\` ${this.model}:\n${formatObject(args)}`);
1580+
}
1581+
1582+
// Prisma Pulse returns an async iterable, which we need to wrap
1583+
// and post-process the iteration results
1584+
const iterable = await this.modelClient[action](args);
1585+
return {
1586+
[Symbol.asyncIterator]: () => {
1587+
const iter = iterable[Symbol.asyncIterator].bind(iterable)();
1588+
return {
1589+
next: async () => {
1590+
const { done, value } = await iter.next();
1591+
let processedValue = value;
1592+
if (value && 'action' in value) {
1593+
switch (value.action) {
1594+
case 'create':
1595+
if ('created' in value) {
1596+
processedValue = {
1597+
...value,
1598+
created: this.policyUtils.postProcessForRead(value.created, this.model, {}),
1599+
};
1600+
}
1601+
break;
1602+
1603+
case 'update':
1604+
if ('before' in value) {
1605+
processedValue = {
1606+
...value,
1607+
before: this.policyUtils.postProcessForRead(value.before, this.model, {}),
1608+
};
1609+
}
1610+
if ('after' in value) {
1611+
processedValue = {
1612+
...value,
1613+
after: this.policyUtils.postProcessForRead(value.after, this.model, {}),
1614+
};
1615+
}
1616+
break;
1617+
1618+
case 'delete':
1619+
if ('deleted' in value) {
1620+
processedValue = {
1621+
...value,
1622+
deleted: this.policyUtils.postProcessForRead(value.deleted, this.model, {}),
1623+
};
1624+
}
1625+
break;
1626+
}
1627+
}
1628+
1629+
return { done, value: processedValue };
1630+
},
1631+
return: () => iter.return?.(),
1632+
throw: () => iter.throw?.(),
1633+
};
1634+
},
1635+
};
15871636
}
15881637

15891638
//#endregion

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,7 @@ export class PolicyUtil extends QueryUtils {
595595
// make select and include visible to the injection
596596
const injected: any = { select: args.select, include: args.include };
597597
if (!this.injectAuthGuardAsWhere(db, injected, model, 'read')) {
598+
args.where = this.makeFalse();
598599
return false;
599600
}
600601

packages/runtime/src/enhancements/proxy.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ export interface PrismaProxyHandler {
5454
count(args: any): Promise<unknown | number>;
5555

5656
subscribe(args: any): Promise<unknown>;
57+
58+
stream(args: any): Promise<unknown>;
5759
}
5860

5961
/**
@@ -79,7 +81,7 @@ export class DefaultPrismaProxyHandler implements PrismaProxyHandler {
7981
async () => {
8082
args = await this.preprocessArgs(method, args);
8183
const r = await this.prisma[this.model][method](args);
82-
return postProcess ? this.processResultEntity(r) : r;
84+
return postProcess ? this.processResultEntity(method, r) : r;
8385
},
8486
args,
8587
this.options.modelMeta,
@@ -92,7 +94,7 @@ export class DefaultPrismaProxyHandler implements PrismaProxyHandler {
9294
return createDeferredPromise<TResult>(async () => {
9395
args = await this.preprocessArgs(method, args);
9496
const r = await this.prisma[this.model][method](args);
95-
return postProcess ? this.processResultEntity(r) : r;
97+
return postProcess ? this.processResultEntity(method, r) : r;
9698
});
9799
}
98100

@@ -161,20 +163,44 @@ export class DefaultPrismaProxyHandler implements PrismaProxyHandler {
161163
}
162164

163165
subscribe(args: any) {
164-
return this.deferred('subscribe', args, false);
166+
return this.doSubscribeStream('subscribe', args);
167+
}
168+
169+
stream(args: any) {
170+
return this.doSubscribeStream('stream', args);
171+
}
172+
173+
private async doSubscribeStream(method: 'subscribe' | 'stream', args: any) {
174+
// Prisma's `subscribe` and `stream` methods return an async iterable
175+
// which we need to wrap to process the iteration results
176+
const iterable = await this.prisma[this.model][method](args);
177+
return {
178+
[Symbol.asyncIterator]: () => {
179+
const iter = iterable[Symbol.asyncIterator].bind(iterable)();
180+
return {
181+
next: async () => {
182+
const { done, value } = await iter.next();
183+
const processedValue = value ? await this.processResultEntity(method, value) : value;
184+
return { done, value: processedValue };
185+
},
186+
return: () => iter.return?.(),
187+
throw: () => iter.throw?.(),
188+
};
189+
},
190+
};
165191
}
166192

167193
/**
168194
* Processes result entities before they're returned
169195
*/
170-
protected async processResultEntity<T>(data: T): Promise<T> {
196+
protected async processResultEntity<T>(_method: PrismaProxyActions, data: T): Promise<T> {
171197
return data;
172198
}
173199

174200
/**
175201
* Processes query args before they're passed to Prisma.
176202
*/
177-
protected async preprocessArgs(method: PrismaProxyActions, args: any) {
203+
protected async preprocessArgs(_method: PrismaProxyActions, args: any) {
178204
return args;
179205
}
180206
}

packages/runtime/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface DbOperations {
2323
groupBy(args: unknown): Promise<any>;
2424
count(args?: unknown): Promise<any>;
2525
subscribe(args?: unknown): Promise<any>;
26+
stream(args?: unknown): Promise<any>;
2627
check(args: unknown): Promise<boolean>;
2728
fields: Record<string, any>;
2829
}

packages/testtools/src/schema.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) {
237237
}
238238

239239
if (opt.pushDb) {
240-
run('npx prisma db push --skip-generate');
240+
run('npx prisma db push --skip-generate --accept-data-loss');
241241
}
242242

243243
if (opt.pulseApiKey) {
@@ -264,10 +264,10 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) {
264264
// https://github.com/prisma/prisma/issues/18292
265265
prisma[Symbol.for('nodejs.util.inspect.custom')] = 'PrismaClient';
266266

267-
const prismaModule = require(path.join(projectDir, 'node_modules/@prisma/client')).Prisma;
267+
const prismaModule = loadModule('@prisma/client', projectDir).Prisma;
268268

269269
if (opt.pulseApiKey) {
270-
const withPulse = require(path.join(projectDir, 'node_modules/@prisma/extension-pulse/dist/cjs')).withPulse;
270+
const withPulse = loadModule('@prisma/extension-pulse/node', projectDir).withPulse;
271271
prisma = prisma.$extends(withPulse({ apiKey: opt.pulseApiKey }));
272272
}
273273

@@ -388,3 +388,8 @@ export async function loadZModelAndDmmf(
388388
const dmmf = await getDMMF({ datamodel: prismaContent });
389389
return { model, dmmf, modelFile };
390390
}
391+
392+
function loadModule(module: string, basePath: string): any {
393+
const modulePath = require.resolve(module, { paths: [basePath] });
394+
return require(modulePath);
395+
}

0 commit comments

Comments
 (0)