diff --git a/README.md b/README.md index 49ec30f2f..2316cd15b 100644 --- a/README.md +++ b/README.md @@ -264,7 +264,7 @@ export const getServerSideProps: GetServerSideProps = async () => { **Please note** that server-side database access is not protected by access policies. This is by-design so as to provide a way of bypassing the policies. Please make sure you implement authorization properly. -## What's next? +## Learning more ### [Learning the ZModel language](/docs/get-started/learning-the-zmodel-language.md) @@ -272,6 +272,8 @@ export const getServerSideProps: GetServerSideProps = async () => { ### [Database hosting considerations](/docs/ref/database-hosting-considerations.md) +### [Setup logging](/docs/ref/setup-logging.md) + ## Reach out to us for issues, feedback and ideas! [Discord](https://discord.gg/dbuC9ZWc) [Twitter](https://twitter.com/zenstackhq) diff --git a/docs/ref/setup-logging.md b/docs/ref/setup-logging.md new file mode 100644 index 000000000..7e5194e27 --- /dev/null +++ b/docs/ref/setup-logging.md @@ -0,0 +1,75 @@ +# Setup Logging + +ZenStack uses the following levels to control server-side logging: + +1. error + + Error level logging + +1. warn + + Warning level logging + +1. info + + Info level logging + +1. verbose + + Verbose level logging + +1. query + + Detailed database query logging + +By default, ZenStack prints `error` and `warn` level of logging with `console.error` and `console.log`, respectively. You can also control the logging behavior by providing a `zenstack.config.json` file at the root of your project. + +You can turn log levels on and off in `zenstack.config.json`: + +```json +{ + "log": ["verbose", "info", "warn"] +} +``` + +You can also configure ZenStack to emit log as event instead of dumping to stdout, like: + +```json +{ + "log": [ + { + "level": "info", + "emit": "event" + } + ] +} +``` + +To consume the events: + +```ts +import service from '@zenstackhq/runtime'; + +service.$on('info', (event) => { + console.log(event.timestamp, event.message); +}); +``` + +You can also mix and match stdout output with event emitting, like: + +```json +{ + "log": [ + { + "level": "info", + "emit": "stdout" + }, + { + "level": "info", + "emit": "event" + } + ] +} +``` + +The settings in `zenstack.config.json` controls logging of both ZenStack and the underlying Prisma instance. diff --git a/package.json b/package.json index a7e2d3088..5f9c2d7fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "0.2.8", + "version": "0.2.9", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/internal/jest.config.ts b/packages/internal/jest.config.ts index 5fde9b5bf..a04ad4290 100644 --- a/packages/internal/jest.config.ts +++ b/packages/internal/jest.config.ts @@ -10,6 +10,9 @@ export default { // Automatically clear mock calls, instances, contexts and results before every test clearMocks: true, + // Automatically reset mock state before every test + resetMocks: true, + // Indicates whether the coverage information should be collected while executing the test collectCoverage: true, diff --git a/packages/internal/package.json b/packages/internal/package.json index 3a1755157..895906c38 100644 --- a/packages/internal/package.json +++ b/packages/internal/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/internal", - "version": "0.2.8", + "version": "0.2.9", "displayName": "ZenStack Internal Library", "description": "ZenStack internal runtime library. This package is for supporting runtime functionality of ZenStack and not supposed to be used directly.", "repository": { @@ -25,6 +25,7 @@ ], "dependencies": { "bcryptjs": "^2.4.3", + "colors": "^1.4.0", "cuid": "^2.1.8", "deepcopy": "^2.1.0", "swr": "^1.3.0" diff --git a/packages/internal/src/config.ts b/packages/internal/src/config.ts new file mode 100644 index 000000000..dd50cd1ae --- /dev/null +++ b/packages/internal/src/config.ts @@ -0,0 +1,16 @@ +import { LogLevel } from './types'; + +/** + * Logging config definition + */ +export type LogDefinition = { + level: LogLevel; + emit: 'stdout' | 'event'; +}; + +/** + * Service configuration + */ +export interface ServiceConfig { + log?: Array; +} diff --git a/packages/internal/src/handler/data/handler.ts b/packages/internal/src/handler/data/handler.ts index 916c4958c..ee664e292 100644 --- a/packages/internal/src/handler/data/handler.ts +++ b/packages/internal/src/handler/data/handler.ts @@ -49,6 +49,11 @@ export default class DataHandler const context = { user: await this.options.getServerUser(req, res) }; + this.service.verbose(`Data request: ${method} ${path}`); + if (req.body) { + this.service.verbose(`Request body: ${JSON.stringify(req.body)}`); + } + try { switch (method) { case 'GET': @@ -68,12 +73,12 @@ export default class DataHandler break; default: - console.warn(`Unhandled method: ${method}`); + this.service.warn(`Unhandled method: ${method}`); res.status(200).send({}); break; } } catch (err: unknown) { - console.log(`Error handling ${method} ${model}: ${err}`); + this.service.error(`${method} ${model}: ${err}`); if (err instanceof RequestHandlerError) { // in case of errors thrown directly by ZenStack @@ -85,12 +90,14 @@ export default class DataHandler message: err.message, }); break; + case ServerErrorCode.ENTITY_NOT_FOUND: res.status(404).send({ code: err.code, message: err.message, }); break; + default: res.status(400).send({ code: err.code, @@ -112,13 +119,21 @@ export default class DataHandler message: 'an unhandled Prisma error occurred', }); } + } else if (this.isPrismaClientValidationError(err)) { + // prisma validation error + res.status(400).send({ + code: ServerErrorCode.INVALID_REQUEST_PARAMS, + message: getServerErrorMessage( + ServerErrorCode.INVALID_REQUEST_PARAMS + ), + }); } else { // generic errors - console.error( + this.service.error( `An unknown error occurred: ${JSON.stringify(err)}` ); - if (err instanceof Error) { - console.error(err.stack); + if (err instanceof Error && err.stack) { + this.service.error(err.stack); } res.status(500).send({ error: ServerErrorCode.UNKNOWN, @@ -206,7 +221,7 @@ export default class DataHandler ); // conduct the create - console.log( + this.service.verbose( `Conducting create: ${model}:\n${JSON.stringify(args)}` ); const createResult = (await tx[model].create(args)) as { @@ -229,7 +244,7 @@ export default class DataHandler const createdIds = await queryIds(model, tx, { [TRANSACTION_FIELD_NAME]: `${transactionId}:create`, }); - console.log( + this.service.verbose( `Validating nestedly created entities: ${model}#[${createdIds.join( ', ' )}]` @@ -332,7 +347,7 @@ export default class DataHandler ); // conduct the update - console.log( + this.service.verbose( `Conducting update: ${model}:\n${JSON.stringify(args)}` ); await tx[model].update(args); @@ -343,7 +358,7 @@ export default class DataHandler const createdIds = await queryIds(model, tx, { [TRANSACTION_FIELD_NAME]: `${transactionId}:create`, }); - console.log( + this.service.verbose( `Validating nestedly created entities: ${model}#[${createdIds.join( ', ' )}]` @@ -445,7 +460,7 @@ export default class DataHandler } // conduct the deletion - console.log( + this.service.verbose( `Conducting delete ${model}:\n${JSON.stringify(args)}` ); await tx[model].delete(args); @@ -463,8 +478,17 @@ export default class DataHandler } } - private isPrismaClientKnownRequestError(err: any): err is { code: string } { - // we can't reference Prisma generated types so need to weakly check error type - return !!err.clientVersion && typeof err.code === 'string'; + private isPrismaClientKnownRequestError( + err: any + ): err is { code: string; message: string } { + return ( + err.__proto__.constructor.name === 'PrismaClientKnownRequestError' + ); + } + + private isPrismaClientValidationError( + err: any + ): err is { message: string } { + return err.__proto__.constructor.name === 'PrismaClientValidationError'; } } diff --git a/packages/internal/src/handler/data/policy-utils.ts b/packages/internal/src/handler/data/policy-utils.ts index ff22c15e5..051c02f86 100644 --- a/packages/internal/src/handler/data/policy-utils.ts +++ b/packages/internal/src/handler/data/policy-utils.ts @@ -102,7 +102,7 @@ export async function readWithCheck( // recursively inject read guard conditions into the query args await injectNestedReadConditions(model, args, service, context); - console.log( + service.verbose( `Reading with validation for ${model}: ${JSON.stringify(args)}` ); const result = await db[model].findMany(args); @@ -207,7 +207,7 @@ async function postProcessForRead( continue; } - console.log( + service.verbose( `Validating read of to-one relation: ${fieldInfo.type}#${entityData[field].id}` ); @@ -378,7 +378,7 @@ export async function checkPolicyForIds( context: QueryContext, db: Record ) { - console.log( + service.verbose( `Checking policy for ${model}#[${ids.join(', ')}] for ${operation}` ); @@ -427,7 +427,7 @@ async function checkPolicyForSelectionPath( // build a Prisma query for the path const query = buildChainedSelectQuery(id, selectionPath); - console.log( + service.verbose( `Query for selection path: model ${model}, path ${JSON.stringify( selectionPath )}, query ${JSON.stringify(query)}` @@ -436,7 +436,7 @@ async function checkPolicyForSelectionPath( // collect ids at the end of the path const ids: string[] = collectTerminalEntityIds(selectionPath, r); - console.log(`Collected leaf ids: ${JSON.stringify(ids)}`); + service.verbose(`Collected leaf ids: ${JSON.stringify(ids)}`); if (ids.length === 0) { return; diff --git a/packages/internal/src/index.ts b/packages/internal/src/index.ts index e50b82227..27aef584c 100644 --- a/packages/internal/src/index.ts +++ b/packages/internal/src/index.ts @@ -1,3 +1,5 @@ export * from './types'; +export * from './config'; +export * from './service'; export * from './request-handler'; export * as request from './request'; diff --git a/packages/internal/src/request-handler.ts b/packages/internal/src/request-handler.ts index 4c173c7a9..d182e6557 100644 --- a/packages/internal/src/request-handler.ts +++ b/packages/internal/src/request-handler.ts @@ -40,7 +40,8 @@ export function requestHandler( return dataHandler.handle(req, res, rest); default: - res.status(404).json({ error: 'Unknown route: ' + route }); + service.warn(`Unknown route: ${route}`); + res.status(404).json({ error: `Unknown route: ${route}` }); } }; } diff --git a/packages/internal/src/request.ts b/packages/internal/src/request.ts index adea8a1e7..41ea2224a 100644 --- a/packages/internal/src/request.ts +++ b/packages/internal/src/request.ts @@ -113,7 +113,6 @@ export function getMutate(): Mutator { const keys = Array.from(cache.keys()).filter( (k) => typeof k === 'string' && k.startsWith(key) ) as string[]; - console.log('Mutating keys:', JSON.stringify(keys)); const mutations = keys.map((key) => mutate(key, data, opts)); return Promise.all(mutations); }; diff --git a/packages/internal/src/service.ts b/packages/internal/src/service.ts new file mode 100644 index 000000000..994075106 --- /dev/null +++ b/packages/internal/src/service.ts @@ -0,0 +1,182 @@ +import * as fs from 'fs'; +import { EventEmitter } from 'stream'; +import { ServiceConfig } from './config'; +import { + FieldInfo, + LogEvent, + LogLevel, + PolicyOperationKind, + QueryContext, + Service, +} from './types'; +import colors from 'colors'; + +export abstract class DefaultService< + DbClient extends { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + $on: (eventType: any, handler: (event: any) => void) => void; + } +> implements Service +{ + protected config: ServiceConfig; + private prisma: DbClient; + protected readonly logEmitter = new EventEmitter(); + private readonly logSettings = { + query: { stdout: false, emit: false }, + verbose: { stdout: false, emit: false }, + info: { stdout: true, emit: false }, + warn: { stdout: true, emit: false }, + error: { stdout: true, emit: false }, + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private guardModule: any; + + private readonly prismaLogLevels: LogLevel[] = [ + 'query', + 'info', + 'warn', + 'error', + ]; + + constructor() { + this.initialize(); + } + + private initialize() { + this.config = this.loadConfig(); + + // initialize log sink mapping + if (this.config.log) { + // reset all levels + for (const key of Object.keys(this.logSettings)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this.logSettings as any)[key] = { stdout: false, emit: false }; + } + + for (const entry of this.config.log) { + const level = typeof entry === 'string' ? entry : entry.level; + if (!Object.keys(this.logSettings).includes(level)) { + console.error(`Unknown log level "${level}"`); + continue; + } + if (typeof entry === 'string') { + this.logSettings[level].stdout = true; + } else if (entry.emit === 'stdout') { + this.logSettings[level].stdout = true; + } else { + this.logSettings[level].emit = true; + } + } + } + + console.log( + 'Initializing ZenStack service with config:', + JSON.stringify(this.config) + ); + + this.prisma = this.initializePrisma(); + + for (const level of this.prismaLogLevels) { + if (this.logSettings[level].emit) { + this.verbose(`Hooking prisma log level ${level}`); + this.prisma.$on(level, (e) => { + this.logEmitter.emit(level, e); + }); + } + } + } + + $on(level: LogLevel, callback: (event: LogEvent) => void): void { + this.logEmitter.on(level, callback); + } + + private handleLog(level: LogLevel, message: string): void { + if (this.logSettings[level].stdout) { + switch (level) { + case 'verbose': + console.log(colors.blue(`zenstack:${level}`), message); + break; + case 'info': + console.log(colors.cyan(`zenstack:${level}`), message); + break; + case 'warn': + console.warn(colors.yellow(`zenstack:${level}`), message); + break; + case 'error': + console.error(colors.red(`zenstack:${level}`), message); + break; + } + } + if (this.logSettings[level].emit) { + this.logEmitter.emit(level, { timestamp: new Date(), message }); + } + } + + private loadConfig(): ServiceConfig { + const configFile = './zenstack.config.json'; + if (fs.existsSync(configFile)) { + try { + const config = JSON.parse( + fs.readFileSync(configFile).toString('utf-8') + ); + return config as ServiceConfig; + } catch (err) { + console.error('Failed to load zenstack.config.json', err); + } + } + return {}; + } + + get db(): DbClient { + return this.prisma; + } + + async resolveField( + model: string, + field: string + ): Promise { + if (!this.guardModule) { + this.guardModule = await this.loadGuardModule(); + } + return this.guardModule._fieldMapping?.[model]?.[field]; + } + + async buildQueryGuard( + model: string, + operation: PolicyOperationKind, + context: QueryContext + ): Promise { + if (!this.guardModule) { + this.guardModule = await this.loadGuardModule(); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const provider: (context: QueryContext) => any = + this.guardModule[model + '_' + operation]; + return provider(context); + } + + verbose(message: string): void { + this.handleLog('verbose', message); + } + + info(message: string): void { + this.handleLog('info', message); + } + + warn(message: string): void { + this.handleLog('warn', message); + } + + error(message: string): void { + this.handleLog('error', message); + } + + reinitialize(): void { + this.initialize(); + } + + protected abstract initializePrisma(): DbClient; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected abstract loadGuardModule(): Promise; +} diff --git a/packages/internal/src/types.ts b/packages/internal/src/types.ts index 9f0a01b69..214ab49b6 100644 --- a/packages/internal/src/types.ts +++ b/packages/internal/src/types.ts @@ -62,6 +62,11 @@ export type DbClientContract = Record & { ) => Promise; }; +/** + * Logging levels + */ +export type LogLevel = 'verbose' | 'info' | 'query' | 'warn' | 'error'; + /** * The main service of ZenStack. Implementation of this interface is automatically generated. */ @@ -93,6 +98,31 @@ export interface Service { operation: PolicyOperationKind, context: QueryContext ): Promise; + + /** + * Generates a log message with verbose level. + */ + verbose(message: string): void; + + /** + * Generates a log message with info level. + */ + info(message: string): void; + + /** + * Generates a log message with warn level. + */ + warn(message: string): void; + + /** + * Generates a log message with error level. + */ + error(message: string): void; + + /** + * Registers a listener to log events. + */ + $on(level: LogLevel, callback: (event: LogEvent) => void): void; } /** @@ -162,3 +192,18 @@ export function getServerErrorMessage(code: ServerErrorCode): string { return `generic error: ${code}`; } } + +export type LogEventHandler = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + LogEvent: any, + handler: (event: LogEvent) => void +) => void; + +export type LogEvent = { + timestamp: Date; + query?: string; + params?: string; + duration?: number; + target?: string; + message?: string; +}; diff --git a/packages/internal/tsconfig.json b/packages/internal/tsconfig.json index f58f60c5e..5f19b5c6d 100644 --- a/packages/internal/tsconfig.json +++ b/packages/internal/tsconfig.json @@ -14,6 +14,7 @@ "forceConsistentCasingInFileNames": true, "declaration": true, "resolveJsonModule": true, + "strictPropertyInitialization": false, "paths": {} }, "include": ["src/**/*.ts"], diff --git a/packages/runtime/package.json b/packages/runtime/package.json index c7da9dc11..0da45ec7b 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "0.2.8", + "version": "0.2.9", "description": "This package contains runtime library for consuming client and server side code generated by ZenStack.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index 984c6fe8f..3dbe8e7bd 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack Language Tools", "description": "ZenStack is a toolkit that simplifies full-stack development", - "version": "0.2.8", + "version": "0.2.9", "author": { "name": "ZenStack Team" }, @@ -65,9 +65,9 @@ "main": "./bundle/extension.js", "scripts": { "vscode:publish": "vsce publish --no-dependencies", - "vscode:prepublish": "cp ../../README.md ./ && pnpm lint && pnpm build && pnpm bundle", + "vscode:prepublish": "cp ../../README.md ./ && pnpm lint && pnpm build", "vscode:package": "vsce package --no-dependencies", - "build": "cp -r src/res/* bundle/res/ && pnpm langium:generate && tsc --noEmit", + "build": "pnpm langium:generate && tsc --noEmit && pnpm bundle && cp -r src/res/* bundle/res/", "bundle": "node build/bundle.js --minify", "bundle-watch": "node build/bundle.js --watch", "ts:watch": "tsc --watch --noEmit", diff --git a/packages/schema/src/generator/next-auth/index.ts b/packages/schema/src/generator/next-auth/index.ts index f6a7b5771..0af334c9e 100644 --- a/packages/schema/src/generator/next-auth/index.ts +++ b/packages/schema/src/generator/next-auth/index.ts @@ -3,6 +3,7 @@ import { Project } from 'ts-morph'; import * as path from 'path'; import colors from 'colors'; import { DataModel, isDataModel, Model } from '@lang/generated/ast'; +import { execSync } from 'child_process'; /** * Generates NextAuth adaptor code @@ -20,7 +21,7 @@ export default class NextAuthGenerator implements Generator { async generate(context: Context): Promise { try { - require('next-auth'); + execSync('npm ls next-auth'); } catch (err) { console.warn( colors.yellow( diff --git a/packages/schema/src/generator/service/index.ts b/packages/schema/src/generator/service/index.ts index c80ec193b..167b42905 100644 --- a/packages/schema/src/generator/service/index.ts +++ b/packages/schema/src/generator/service/index.ts @@ -1,5 +1,5 @@ import { Context, Generator } from '../types'; -import { Project, StructureKind, VariableDeclarationKind } from 'ts-morph'; +import { Project } from 'ts-morph'; import * as path from 'path'; import colors from 'colors'; import { INTERNAL_PACKAGE } from '../constants'; @@ -16,90 +16,31 @@ export default class ServiceGenerator implements Generator { { overwrite: true } ); - sf.addImportDeclaration({ - namedImports: ['PrismaClient'], - moduleSpecifier: '../.prisma', - }); - - sf.addImportDeclaration({ - namedImports: ['Service', 'PolicyOperationKind', 'QueryContext'], - moduleSpecifier: INTERNAL_PACKAGE, - isTypeOnly: true, - }); - - sf.addVariableStatement({ - declarationKind: VariableDeclarationKind.Let, - declarations: [ - { - name: 'guardModule', - type: 'any', - }, - ], - }); + sf.addStatements([ + `import { PrismaClient } from "../.prisma";`, + `import { DefaultService } from "${INTERNAL_PACKAGE}";`, + ]); const cls = sf.addClass({ name: 'ZenStackService', isExported: true, - implements: ['Service'], - }); - cls.addMember({ - kind: StructureKind.Property, - name: 'private readonly _prisma', - initializer: 'new PrismaClient()', + extends: 'DefaultService', }); - cls.addGetAccessor({ - name: 'db', - }) - .addBody() - .setBodyText('return this._prisma;'); - - cls - .addMethod({ - name: 'resolveField', - isAsync: true, - parameters: [ - { - name: 'model', - type: 'string', - }, - { - name: 'field', - type: 'string', - }, - ], - }) - .addBody().setBodyText(` - if (!guardModule) { - guardModule = await import('./query/guard'); - } - return guardModule._fieldMapping?.[model]?.[field]; - `); - - cls - .addMethod({ - name: 'buildQueryGuard', - isAsync: true, - parameters: [ - { - name: 'model', - type: 'string', - }, - { - name: 'operation', - type: 'PolicyOperationKind', - }, - { - name: 'context', - type: 'QueryContext', - }, - ], - }) - .addBody().setBodyText(` - const module: any = await import('./query/guard'); - const provider: (context: QueryContext) => any = module[model+ '_' + operation]; - return provider(context); - `); + cls.addMethod({ + name: 'initializePrisma', + }).setBodyText(` + const logConfig = (this.config.log || []) + .filter(item => typeof item === 'string' ? ['info', 'warn', 'error', 'query'].includes(item): ['info', 'warn', 'error', 'query'].includes(item.level)); + return new PrismaClient({log: logConfig as any }); + `); + + cls.addMethod({ + name: 'loadGuardModule', + isAsync: true, + }).setBodyText(` + return import('./query/guard'); + `); // Recommended by Prisma for Next.js // https://www.prisma.io/docs/guides/database/troubleshooting-orm/help-articles/nextjs-prisma-client-dev-practices#problem diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 65a8fd4cf..f6f0d47da 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,6 +13,7 @@ importers: '@types/node': ^14.18.29 '@types/uuid': ^8.3.4 bcryptjs: ^2.4.3 + colors: ^1.4.0 cuid: ^2.1.8 deepcopy: ^2.1.0 jest: ^29.0.3 @@ -27,9 +28,10 @@ importers: typescript: ^4.6.2 dependencies: bcryptjs: 2.4.3 + colors: 1.4.0 cuid: 2.1.8 deepcopy: 2.1.0 - next: 12.3.1_6tziyx3dehkoeijunclpkpolha + next: 12.3.1_qtpcxnaaarbm4ws7ughq6oxfve react: 18.2.0 react-dom: 18.2.0_react@18.2.0 swr: 1.3.0_react@18.2.0 @@ -145,6 +147,7 @@ importers: bcryptjs: ^2.4.3 jest: ^29.0.3 next: ^12.3.1 + sleep-promise: ^9.1.0 supertest: ^6.3.0 tmp: ^0.2.1 ts-jest: ^29.0.1 @@ -153,16 +156,17 @@ importers: dependencies: '@types/node': 14.18.29 bcryptjs: 2.4.3 + sleep-promise: 9.1.0 devDependencies: '@types/bcryptjs': 2.4.2 '@types/jest': 29.0.3 '@types/supertest': 2.0.12 '@types/tmp': 0.2.3 jest: 29.0.3_johvxhudwcpndp4mle25vwrlq4 - next: 12.3.1_6tziyx3dehkoeijunclpkpolha + next: 12.3.1_qtpcxnaaarbm4ws7ughq6oxfve supertest: 6.3.0 tmp: 0.2.1 - ts-jest: 29.0.1_poggjixajg6vd6yquly7s7dsj4 + ts-jest: 29.0.1_t3cec5bure72u77t3utxqeumoa ts-node: 10.9.1_ck2axrxkiif44rdbzjywaqjysa typescript: 4.8.3 @@ -212,6 +216,7 @@ packages: semver: 6.3.0 transitivePeerDependencies: - supports-color + dev: true /@babel/core/7.19.6: resolution: {integrity: sha512-D2Ue4KHpc6Ys2+AxpIx1BZ8+UegLLLE2p3KJEuJRKmokHOtl49jQ5ny1773KsGLZs8MQvBidAF6yWUJxRqtKtg==} @@ -234,7 +239,6 @@ packages: semver: 6.3.0 transitivePeerDependencies: - supports-color - dev: true /@babel/generator/7.19.5: resolution: {integrity: sha512-DxbNz9Lz4aMZ99qPpO1raTbcrI1ZeYh+9NR9qhfkQIbFtVEqotHojEBxHzmxhVONkGt6VyrqVQcgpefMy9pqcg==} @@ -243,6 +247,7 @@ packages: '@babel/types': 7.19.4 '@jridgewell/gen-mapping': 0.3.2 jsesc: 2.5.2 + dev: true /@babel/generator/7.19.6: resolution: {integrity: sha512-oHGRUQeoX1QrKeJIKVe0hwjGqNnVYsM5Nep5zo0uE0m42sLH+Fsd2pStJ5sRM1bNyTUUoz0pe2lTeMJrb/taTA==} @@ -263,6 +268,7 @@ packages: '@babel/helper-validator-option': 7.18.6 browserslist: 4.21.4 semver: 6.3.0 + dev: true /@babel/helper-compilation-targets/7.19.3_@babel+core@7.19.6: resolution: {integrity: sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==} @@ -275,7 +281,6 @@ packages: '@babel/helper-validator-option': 7.18.6 browserslist: 4.21.4 semver: 6.3.0 - dev: true /@babel/helper-environment-visitor/7.18.9: resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==} @@ -314,6 +319,7 @@ packages: '@babel/types': 7.19.4 transitivePeerDependencies: - supports-color + dev: true /@babel/helper-module-transforms/7.19.6: resolution: {integrity: sha512-fCmcfQo/KYr/VXXDIyd3CBGZ6AFhPFy1TfSEJ+PilGVlQT6jcbqtHAM4C1EciRqMza7/TpOUZliuSH+U6HAhJw==} @@ -329,7 +335,6 @@ packages: '@babel/types': 7.19.4 transitivePeerDependencies: - supports-color - dev: true /@babel/helper-plugin-utils/7.19.0: resolution: {integrity: sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw==} @@ -341,13 +346,13 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.19.4 + dev: true /@babel/helper-simple-access/7.19.4: resolution: {integrity: sha512-f9Xq6WqBFqaDfbCzn2w85hwklswz5qsKlh7f08w4Y9yhJHpnNC0QemtSkK5YyOY8kPGvyiwdzZksGUhnGdaUIg==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.19.4 - dev: true /@babel/helper-split-export-declaration/7.18.6: resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==} @@ -391,6 +396,7 @@ packages: hasBin: true dependencies: '@babel/types': 7.19.4 + dev: true /@babel/parser/7.19.6: resolution: {integrity: sha512-h1IUp81s2JYJ3mRkdxJgs4UvmSsRvDrx5ICSJbPvtWYv5i1nTBGcBpnog+89rAFMwvvru6E5NUHdBe01UeSzYA==} @@ -681,6 +687,7 @@ packages: globals: 11.12.0 transitivePeerDependencies: - supports-color + dev: true /@babel/traverse/7.19.6: resolution: {integrity: sha512-6l5HrUCzFM04mfbG09AagtYyR2P0B71B1wN7PfSPiksDPz2k5H9CBC1tcZpz2M8OxbKTPccByoOJ22rUKbpmQQ==} @@ -5045,7 +5052,7 @@ packages: engines: {node: '>=10'} dev: true - /next/12.3.1_6tziyx3dehkoeijunclpkpolha: + /next/12.3.1_qtpcxnaaarbm4ws7ughq6oxfve: resolution: {integrity: sha512-l7bvmSeIwX5lp07WtIiP9u2ytZMv7jIeB8iacR28PuUEFG5j0HGAPnMqyG5kbZNBG2H7tRsrQ4HCjuMOPnANZw==} engines: {node: '>=12.22.0'} hasBin: true @@ -5069,7 +5076,7 @@ packages: postcss: 8.4.14 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 - styled-jsx: 5.0.7_b6k74wvxdvqypha4emuv7fd2ke + styled-jsx: 5.0.7_otspjrsspon4ofp37rshhlhp2y use-sync-external-store: 1.2.0_react@18.2.0 optionalDependencies: '@next/swc-android-arm-eabi': 12.3.1 @@ -5773,6 +5780,10 @@ packages: engines: {node: '>=8'} dev: true + /sleep-promise/9.1.0: + resolution: {integrity: sha512-UHYzVpz9Xn8b+jikYSD6bqvf754xL2uBUzDFwiU6NcdZeifPr6UfgU43xpkPu67VMS88+TI2PSI7Eohgqf2fKA==} + dev: false + /slice-ansi/3.0.0: resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} engines: {node: '>=8'} @@ -5914,7 +5925,7 @@ packages: engines: {node: '>=8'} dev: true - /styled-jsx/5.0.7_b6k74wvxdvqypha4emuv7fd2ke: + /styled-jsx/5.0.7_otspjrsspon4ofp37rshhlhp2y: resolution: {integrity: sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA==} engines: {node: '>= 12.0.0'} peerDependencies: @@ -5927,7 +5938,7 @@ packages: babel-plugin-macros: optional: true dependencies: - '@babel/core': 7.19.3 + '@babel/core': 7.19.6 react: 18.2.0 /superagent/8.0.2: @@ -6148,6 +6159,40 @@ packages: yargs-parser: 21.1.1 dev: true + /ts-jest/29.0.1_t3cec5bure72u77t3utxqeumoa: + resolution: {integrity: sha512-htQOHshgvhn93QLxrmxpiQPk69+M1g7govO1g6kf6GsjCv4uvRV0znVmDrrvjUrVCnTYeY4FBxTYYYD4airyJA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + '@babel/core': 7.19.6 + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + jest: 29.0.3_johvxhudwcpndp4mle25vwrlq4 + jest-util: 29.0.3 + json5: 2.2.1 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.3.7 + typescript: 4.8.3 + yargs-parser: 21.1.1 + dev: true + /ts-jest/29.0.3_nvckv3qbfhmmsla6emqlkyje4a: resolution: {integrity: sha512-Ibygvmuyq1qp/z3yTh9QTwVVAbFdDy/+4BtIQR2sp6baF2SJU/8CKK/hhnGIDY2L90Az2jIqTwZPnN2p+BweiQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} diff --git a/samples/todo/package-lock.json b/samples/todo/package-lock.json index fe3e8b7bd..244c5595a 100644 --- a/samples/todo/package-lock.json +++ b/samples/todo/package-lock.json @@ -1,22 +1,23 @@ { "name": "todo", - "version": "0.2.4", + "version": "0.2.9", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "todo", - "version": "0.2.4", + "version": "0.2.9", "dependencies": { "@heroicons/react": "^2.0.12", "@prisma/client": "^4.4.0", - "@zenstackhq/internal": "^0.2.4", - "@zenstackhq/runtime": "^0.2.4", + "@zenstackhq/internal": "^0.2.9", + "@zenstackhq/runtime": "^0.2.9", + "bcryptjs": "^2.4.3", "daisyui": "^2.31.0", "moment": "^2.29.4", "nanoid": "^4.0.0", "next": "12.3.1", - "next-auth": "^4.10.3", + "next-auth": "^4.15.1", "react": "18.2.0", "react-dom": "18.2.0", "react-toastify": "^9.0.8", @@ -24,6 +25,7 @@ }, "devDependencies": { "@tailwindcss/line-clamp": "^0.4.2", + "@types/bcryptjs": "^2.4.2", "@types/node": "^14.17.3", "@types/react": "18.0.21", "@types/react-dom": "18.0.6", @@ -33,7 +35,7 @@ "postcss": "^8.4.16", "tailwindcss": "^3.1.8", "typescript": "^4.6.2", - "zenstack": "^0.2.4" + "zenstack": "^0.2.9" } }, "node_modules/@babel/code-frame": { @@ -563,8 +565,7 @@ "node_modules/@types/bcryptjs": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz", - "integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==", - "peer": true + "integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==" }, "node_modules/@types/json5": { "version": "0.0.29", @@ -721,11 +722,12 @@ } }, "node_modules/@zenstackhq/internal": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@zenstackhq/internal/-/internal-0.2.4.tgz", - "integrity": "sha512-+Rg3Jq3RPaRav8zKxqyyrGLqP5JHxo1JEiXxTQHYqhAxdgtzzTOGkVAXMuB+OP/PTpe/6EBHwyd4T2CVyyQfcw==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@zenstackhq/internal/-/internal-0.2.9.tgz", + "integrity": "sha512-Hq+JfXZ9s0v9m085V53kSwlLcZXlNMktuU4DHjiIlPFkTBssYZXphg0FxKHEhfA+FQct1vwDagIAB0UsZyYScg==", "dependencies": { "bcryptjs": "^2.4.3", + "colors": "^1.4.0", "cuid": "^2.1.8", "deepcopy": "^2.1.0", "swr": "^1.3.0" @@ -738,9 +740,9 @@ } }, "node_modules/@zenstackhq/runtime": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@zenstackhq/runtime/-/runtime-0.2.4.tgz", - "integrity": "sha512-6rgbQeYjdI+Eiqp2ubp8ZocIRz2EsLY+zm3OOypoVgjxTFs+gqr2Z3hdWgwSjnEoZBoh2laFJ0tPI4rbHF8lww==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@zenstackhq/runtime/-/runtime-0.2.9.tgz", + "integrity": "sha512-l2DqjAqKQe4bCyu0mta1WKtDKNE4/VH51n4UfOTy6xCTSqmTTqTRdEqWypC2u52yMzrRb8re5c749HN8B0OL7A==", "dependencies": { "@zenstackhq/internal": "latest" }, @@ -1262,7 +1264,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "dev": true, "engines": { "node": ">=0.1.90" } @@ -2695,9 +2696,9 @@ "dev": true }, "node_modules/jose": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.10.0.tgz", - "integrity": "sha512-KEhB/eLGLomWGPTb+/RNbYsTjIyx03JmbqAyIyiXBuNSa7CmNrJd5ysFhblayzs/e/vbOPMUaLnjHUMhGp4yLw==", + "version": "4.10.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.10.4.tgz", + "integrity": "sha512-eBH77Xs9Yc/oTDvukhAEDVMijhekPuNktXJL4tUlB22jqKP1k48v5nmsUmc8feoJPsxB3HsfEt2LbVSoz+1mng==", "funding": { "url": "https://github.com/sponsors/panva" } @@ -2987,9 +2988,9 @@ } }, "node_modules/next-auth": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.14.0.tgz", - "integrity": "sha512-pD5sin6kq/uIx3Cod2/0JFnViEnngBTTNy4CdfRaYc2QzV2zwpWAbQny2Ezlg0GjEozDhKC53JJxRRE4AmNKEw==", + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.15.1.tgz", + "integrity": "sha512-yB/cSEqslaXAsLZ3lvkJbB7bRR5JeZvfUXSw0CmWeP+TaMZWB85fG9PWdfBeFrDCLBsoyATw9FF6fzApE0SxSw==", "dependencies": { "@babel/runtime": "^7.16.3", "@panva/hkdf": "^1.0.1", @@ -3002,10 +3003,10 @@ "uuid": "^8.3.2" }, "engines": { - "node": "^12.19.0 || ^14.15.0 || ^16.13.0" + "node": "^12.19.0 || ^14.15.0 || ^16.13.0 || ^18.12.0" }, "peerDependencies": { - "next": "^12.2.5", + "next": "^12.2.5 || ^13", "nodemailer": "^6.6.5", "react": "^17.0.2 || ^18", "react-dom": "^17.0.2 || ^18" @@ -3016,14 +3017,6 @@ } } }, - "node_modules/next-auth/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/next/node_modules/nanoid": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", @@ -3226,9 +3219,9 @@ } }, "node_modules/openid-client": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.2.0.tgz", - "integrity": "sha512-fswY95ZqQr8xBgnlcC9TBJs/QeZAANRaiDliIwHktvpxpWhv5+cfd41OFNflI/ycf09HnqEPWbGrmvSOPgD3HQ==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.2.1.tgz", + "integrity": "sha512-KPxqWnxobG/70Cxqyvd43RWfCfHedFnCdHSBpw5f7WnTnuBAeBnvot/BIo+brrcTr0wyAYUlL/qejQSGwWtdIg==", "dependencies": { "jose": "^4.10.0", "lru-cache": "^6.0.0", @@ -3510,9 +3503,9 @@ } }, "node_modules/preact-render-to-string": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.5.tgz", - "integrity": "sha512-rEBn42C3Wh+AjPxXUbDkb6xw0cTJQgxdYlp6ytUR1uBZF647Wn6ykkopMeQlRl7ggX+qnYYjZ4Hs1abZENl7ww==", + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz", + "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==", "dependencies": { "pretty-format": "^3.8.0" }, @@ -4377,10 +4370,9 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", - "dev": true, + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "bin": { "uuid": "dist/bin/uuid" } @@ -4528,12 +4520,12 @@ } }, "node_modules/zenstack": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/zenstack/-/zenstack-0.2.4.tgz", - "integrity": "sha512-+FjPCiBmy9T/bERNqPZNN6B8Clj7dHKZg2vdyBKSLjC+tLZZsB6eGW1FlPaj3qdQp7zlxvacF8nvwnO1BwEWyg==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/zenstack/-/zenstack-0.2.9.tgz", + "integrity": "sha512-qLEM04i5T2rByrgCf8AWgc1LvBa4f3+IsxxBK+DtwiRaNPfYaZB7Z7Nck1vLHlu8S2eas/Sxm+DmeCiX5NlsyA==", "dev": true, "dependencies": { - "@zenstackhq/internal": "0.2.4", + "@zenstackhq/internal": "0.2.9", "change-case": "^4.1.2", "chevrotain": "^9.1.0", "colors": "^1.4.0", @@ -4556,6 +4548,15 @@ "engines": { "vscode": "^1.72.0" } + }, + "node_modules/zenstack/node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } } }, "dependencies": { @@ -4908,8 +4909,7 @@ "@types/bcryptjs": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz", - "integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==", - "peer": true + "integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==" }, "@types/json5": { "version": "0.0.29", @@ -5017,20 +5017,21 @@ } }, "@zenstackhq/internal": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@zenstackhq/internal/-/internal-0.2.4.tgz", - "integrity": "sha512-+Rg3Jq3RPaRav8zKxqyyrGLqP5JHxo1JEiXxTQHYqhAxdgtzzTOGkVAXMuB+OP/PTpe/6EBHwyd4T2CVyyQfcw==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@zenstackhq/internal/-/internal-0.2.9.tgz", + "integrity": "sha512-Hq+JfXZ9s0v9m085V53kSwlLcZXlNMktuU4DHjiIlPFkTBssYZXphg0FxKHEhfA+FQct1vwDagIAB0UsZyYScg==", "requires": { "bcryptjs": "^2.4.3", + "colors": "^1.4.0", "cuid": "^2.1.8", "deepcopy": "^2.1.0", "swr": "^1.3.0" } }, "@zenstackhq/runtime": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@zenstackhq/runtime/-/runtime-0.2.4.tgz", - "integrity": "sha512-6rgbQeYjdI+Eiqp2ubp8ZocIRz2EsLY+zm3OOypoVgjxTFs+gqr2Z3hdWgwSjnEoZBoh2laFJ0tPI4rbHF8lww==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@zenstackhq/runtime/-/runtime-0.2.9.tgz", + "integrity": "sha512-l2DqjAqKQe4bCyu0mta1WKtDKNE4/VH51n4UfOTy6xCTSqmTTqTRdEqWypC2u52yMzrRb8re5c749HN8B0OL7A==", "requires": { "@zenstackhq/internal": "latest" } @@ -5401,8 +5402,7 @@ "colors": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "dev": true + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" }, "commander": { "version": "8.3.0", @@ -6470,9 +6470,9 @@ "dev": true }, "jose": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.10.0.tgz", - "integrity": "sha512-KEhB/eLGLomWGPTb+/RNbYsTjIyx03JmbqAyIyiXBuNSa7CmNrJd5ysFhblayzs/e/vbOPMUaLnjHUMhGp4yLw==" + "version": "4.10.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.10.4.tgz", + "integrity": "sha512-eBH77Xs9Yc/oTDvukhAEDVMijhekPuNktXJL4tUlB22jqKP1k48v5nmsUmc8feoJPsxB3HsfEt2LbVSoz+1mng==" }, "js-tokens": { "version": "4.0.0", @@ -6699,9 +6699,9 @@ } }, "next-auth": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.14.0.tgz", - "integrity": "sha512-pD5sin6kq/uIx3Cod2/0JFnViEnngBTTNy4CdfRaYc2QzV2zwpWAbQny2Ezlg0GjEozDhKC53JJxRRE4AmNKEw==", + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.15.1.tgz", + "integrity": "sha512-yB/cSEqslaXAsLZ3lvkJbB7bRR5JeZvfUXSw0CmWeP+TaMZWB85fG9PWdfBeFrDCLBsoyATw9FF6fzApE0SxSw==", "requires": { "@babel/runtime": "^7.16.3", "@panva/hkdf": "^1.0.1", @@ -6712,13 +6712,6 @@ "preact": "^10.6.3", "preact-render-to-string": "^5.1.19", "uuid": "^8.3.2" - }, - "dependencies": { - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - } } }, "no-case": { @@ -6844,9 +6837,9 @@ } }, "openid-client": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.2.0.tgz", - "integrity": "sha512-fswY95ZqQr8xBgnlcC9TBJs/QeZAANRaiDliIwHktvpxpWhv5+cfd41OFNflI/ycf09HnqEPWbGrmvSOPgD3HQ==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.2.1.tgz", + "integrity": "sha512-KPxqWnxobG/70Cxqyvd43RWfCfHedFnCdHSBpw5f7WnTnuBAeBnvot/BIo+brrcTr0wyAYUlL/qejQSGwWtdIg==", "requires": { "jose": "^4.10.0", "lru-cache": "^6.0.0", @@ -7029,9 +7022,9 @@ "integrity": "sha512-skAwGDFmgxhq1DCBHke/9e12ewkhc7WYwjuhHB8HHS8zkdtITXLRmUMTeol2ldxvLwYtwbFeifZ9uDDWuyL4Iw==" }, "preact-render-to-string": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.5.tgz", - "integrity": "sha512-rEBn42C3Wh+AjPxXUbDkb6xw0cTJQgxdYlp6ytUR1uBZF647Wn6ykkopMeQlRl7ggX+qnYYjZ4Hs1abZENl7ww==", + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz", + "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==", "requires": { "pretty-format": "^3.8.0" } @@ -7653,10 +7646,9 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", - "dev": true + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, "v8-compile-cache": { "version": "2.3.0", @@ -7774,12 +7766,12 @@ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" }, "zenstack": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/zenstack/-/zenstack-0.2.4.tgz", - "integrity": "sha512-+FjPCiBmy9T/bERNqPZNN6B8Clj7dHKZg2vdyBKSLjC+tLZZsB6eGW1FlPaj3qdQp7zlxvacF8nvwnO1BwEWyg==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/zenstack/-/zenstack-0.2.9.tgz", + "integrity": "sha512-qLEM04i5T2rByrgCf8AWgc1LvBa4f3+IsxxBK+DtwiRaNPfYaZB7Z7Nck1vLHlu8S2eas/Sxm+DmeCiX5NlsyA==", "dev": true, "requires": { - "@zenstackhq/internal": "0.2.4", + "@zenstackhq/internal": "0.2.9", "change-case": "^4.1.2", "chevrotain": "^9.1.0", "colors": "^1.4.0", @@ -7795,6 +7787,14 @@ "vscode-languageserver": "^8.0.2", "vscode-languageserver-textdocument": "^1.0.7", "vscode-uri": "^3.0.6" + }, + "dependencies": { + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "dev": true + } } } } diff --git a/samples/todo/package.json b/samples/todo/package.json index 8a89ca097..16d1f3739 100644 --- a/samples/todo/package.json +++ b/samples/todo/package.json @@ -1,6 +1,6 @@ { "name": "todo", - "version": "0.2.8", + "version": "0.2.9", "private": true, "scripts": { "dev": "next dev", @@ -17,13 +17,14 @@ "dependencies": { "@heroicons/react": "^2.0.12", "@prisma/client": "^4.4.0", - "@zenstackhq/internal": "^0.2.4", - "@zenstackhq/runtime": "^0.2.4", + "@zenstackhq/internal": "^0.2.9", + "@zenstackhq/runtime": "^0.2.9", + "bcryptjs": "^2.4.3", "daisyui": "^2.31.0", "moment": "^2.29.4", "nanoid": "^4.0.0", "next": "12.3.1", - "next-auth": "^4.10.3", + "next-auth": "^4.15.1", "react": "18.2.0", "react-dom": "18.2.0", "react-toastify": "^9.0.8", @@ -31,6 +32,7 @@ }, "devDependencies": { "@tailwindcss/line-clamp": "^0.4.2", + "@types/bcryptjs": "^2.4.2", "@types/node": "^14.17.3", "@types/react": "18.0.21", "@types/react-dom": "18.0.6", @@ -40,6 +42,6 @@ "postcss": "^8.4.16", "tailwindcss": "^3.1.8", "typescript": "^4.6.2", - "zenstack": "^0.2.4" + "zenstack": "^0.2.9" } } diff --git a/samples/todo/tsconfig.json b/samples/todo/tsconfig.json index d65ace5ca..cdfdd1e3e 100644 --- a/samples/todo/tsconfig.json +++ b/samples/todo/tsconfig.json @@ -20,9 +20,9 @@ "@lib/*": ["lib/*"], "@components/*": ["lib/components/*"], "@components": ["lib/components/index"], - "@zenstack/*": [".zenstack/*"] + ".zenstack/*": ["node_modules/.zenstack/*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "zenstack.config.json"], "exclude": ["node_modules"] } diff --git a/tests/integration/jest.config.ts b/tests/integration/jest.config.ts index baae4eb9e..bba1c0b6d 100644 --- a/tests/integration/jest.config.ts +++ b/tests/integration/jest.config.ts @@ -6,10 +6,13 @@ export default { // Automatically clear mock calls, instances, contexts and results before every test clearMocks: true, + // Automatically reset mock state before every test + resetMocks: true, + // A map from regular expressions to paths to transformers transform: { '^.+\\.tsx?$': 'ts-jest' }, - testTimeout: 120000, + testTimeout: 300000, // explicitly specify moduel paths so that resolution from local dependencies work modulePaths: ['/tests/test-run/node_modules'], diff --git a/tests/integration/package.json b/tests/integration/package.json index 8a9a5f4c8..7d79e122b 100644 --- a/tests/integration/package.json +++ b/tests/integration/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "@types/node": "^14.18.29", - "bcryptjs": "^2.4.3" + "bcryptjs": "^2.4.3", + "sleep-promise": "^9.1.0" } } diff --git a/tests/integration/tests/logging.test.ts b/tests/integration/tests/logging.test.ts new file mode 100644 index 000000000..6d0f9eb2b --- /dev/null +++ b/tests/integration/tests/logging.test.ts @@ -0,0 +1,256 @@ +import path from 'path'; +import { makeClient, run, setup } from './utils'; +import * as fs from 'fs'; +import type { DefaultService } from '../../../packages/runtime/server'; + +describe('Logging tests', () => { + let origDir: string; + + beforeAll(async () => { + origDir = path.resolve('.'); + await setup('./tests/todo.zmodel'); + }); + + beforeEach(() => { + run('npx prisma migrate reset --schema ./zenstack/schema.prisma -f'); + }); + + afterAll(() => { + process.chdir(origDir); + }); + + it('logging with default settings', async () => { + const service: DefaultService = require('@zenstackhq/runtime'); + service.reinitialize(); + + let gotInfoEmit = false; + let gotQueryEmit = false; + let gotVerboseEmit = false; + let gotErrorEmit = false; + + let gotInfoStd = false; + let gotQueryStd = false; + let gotVerboseStd = false; + let gotErrorStd = false; + + console.log = jest.fn((...args) => { + const msg = args?.[0] as string; + if (msg.includes('prisma:query')) { + gotQueryStd = true; + } + if (msg.includes(':verbose')) { + gotVerboseStd = true; + } + if (msg.includes(':info')) { + gotInfoStd = true; + } + }); + + console.error = jest.fn((...args) => { + const msg = args?.[0] as string; + if (msg.includes(':error')) { + gotErrorStd = true; + } + }); + + service.$on('info', (event) => { + console.log('Got info', event); + gotInfoEmit = true; + }); + + service.$on('query', (event) => { + console.log('Got query', event); + gotQueryEmit = true; + }); + + service.$on('verbose', (event) => { + console.log('Got verbose', event); + gotVerboseEmit = true; + }); + + service.$on('error', (event) => { + console.log('Got error', event); + gotErrorEmit = true; + }); + + await makeClient('/api/data/User').post('/').send({ + data: {}, + }); + + expect(gotQueryStd).toBeFalsy(); + expect(gotVerboseStd).toBeFalsy(); + expect(gotInfoStd).toBeFalsy(); + expect(gotErrorStd).toBeTruthy(); + + expect(gotInfoEmit).toBeFalsy(); + expect(gotQueryEmit).toBeFalsy(); + expect(gotVerboseEmit).toBeFalsy(); + expect(gotErrorEmit).toBeFalsy(); + }); + + it('logging with stdout', async () => { + fs.writeFileSync( + './zenstack.config.json', + ` + { + "log": ["query", "verbose", "info", "error"] + } + ` + ); + + const service: DefaultService = require('@zenstackhq/runtime'); + service.reinitialize(); + + let gotInfoEmit = false; + let gotQueryEmit = false; + let gotVerboseEmit = false; + let gotErrorEmit = false; + + let gotInfoStd = false; + let gotQueryStd = false; + let gotVerboseStd = false; + let gotErrorStd = false; + + console.log = jest.fn((...args) => { + const msg = args?.[0] as string; + if (msg.includes(':query')) { + gotQueryStd = true; + } + if (msg.includes(':verbose')) { + gotVerboseStd = true; + } + if (msg.includes(':info')) { + gotInfoStd = true; + } + }); + + console.error = jest.fn((...args) => { + const msg = args?.[0] as string; + if (msg.includes(':error')) { + gotErrorStd = true; + } + }); + + service.$on('info', (event) => { + console.log('Got info', event); + gotInfoEmit = true; + }); + + service.$on('query', (event) => { + console.log('Got query', event); + gotQueryEmit = true; + }); + + service.$on('verbose', (event) => { + console.log('Got verbose', event); + gotVerboseEmit = true; + }); + + service.$on('error', (event) => { + console.log('Got error', event); + gotErrorEmit = true; + }); + + await makeClient('/api/data/User').post('/').send({ + data: {}, + }); + + expect(gotQueryStd).toBeTruthy(); + expect(gotVerboseStd).toBeTruthy(); + expect(gotInfoStd).toBeTruthy(); + expect(gotErrorStd).toBeTruthy(); + + expect(gotInfoEmit).toBeFalsy(); + expect(gotQueryEmit).toBeFalsy(); + expect(gotVerboseEmit).toBeFalsy(); + expect(gotErrorEmit).toBeFalsy(); + }); + + it('logging with event', async () => { + fs.writeFileSync( + './zenstack.config.json', + ` + { + "log": [ + { "level": "query", "emit": "event" }, + { "level": "verbose", "emit": "event" }, + { "level": "info", "emit": "event" }, + { "level": "error", "emit": "event" } + ] + } + ` + ); + + const service: DefaultService = require('@zenstackhq/runtime'); + service.reinitialize(); + + let gotInfoEmit = false; + let gotQueryEmit = false; + let gotVerboseEmit = false; + let gotErrorEmit = false; + + let gotInfoStd = false; + let gotQueryStd = false; + let gotVerboseStd = false; + let gotErrorStd = false; + + console.log = jest.fn((...args) => { + const msg = args?.[0] as string; + if (msg.includes(':query')) { + gotQueryStd = true; + } + if (msg.includes(':verbose')) { + gotVerboseStd = true; + } + if (msg.includes(':info')) { + gotInfoStd = true; + } + }); + + console.error = jest.fn((...args) => { + const msg = args?.[0] as string; + if (msg.includes('zenstack:error')) { + gotErrorStd = true; + } + }); + + service.$on('info', (event) => { + expect(event.timestamp).toBeTruthy(); + expect(event.message).toBeTruthy(); + gotInfoEmit = true; + }); + + service.$on('query', (event) => { + expect(event.timestamp).not.toBeUndefined(); + expect(event.query).not.toBeUndefined(); + expect(event.duration).not.toBeUndefined(); + gotQueryEmit = true; + }); + + service.$on('verbose', (event) => { + expect(event.timestamp).not.toBeUndefined(); + expect(event.message).not.toBeUndefined(); + gotVerboseEmit = true; + }); + + service.$on('error', (event) => { + expect(event.timestamp).not.toBeUndefined(); + expect(event.message).not.toBeUndefined(); + gotErrorEmit = true; + }); + + await makeClient('/api/data/User').post('/').send({ + data: {}, + }); + + expect(gotInfoEmit).toBeTruthy(); + expect(gotQueryEmit).toBeTruthy(); + expect(gotVerboseEmit).toBeTruthy(); + expect(gotErrorEmit).toBeTruthy(); + + expect(gotInfoStd).toBeFalsy(); + expect(gotQueryStd).toBeFalsy(); + expect(gotVerboseStd).toBeFalsy(); + expect(gotErrorStd).toBeFalsy(); + }); +});