diff --git a/package.json b/package.json index c4d344673..a7df610c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "0.2.11", + "version": "0.2.12", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/internal/package.json b/packages/internal/package.json index 9ba9462d9..0014dbddf 100644 --- a/packages/internal/package.json +++ b/packages/internal/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/internal", - "version": "0.2.11", + "version": "0.2.12", "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": { @@ -27,12 +27,13 @@ "bcryptjs": "^2.4.3", "colors": "^1.4.0", "cuid": "^2.1.8", + "decimal.js": "^10.4.2", "deepcopy": "^2.1.0", "swr": "^1.3.0" }, "peerDependencies": { "@prisma/client": "^4.4.0", - "next": "12.3.1", + "next": "^12.3.1", "react": "^17.0.2 || ^18", "react-dom": "^17.0.2 || ^18" }, diff --git a/packages/internal/src/handler/data/policy-utils.ts b/packages/internal/src/handler/data/policy-utils.ts index 051c02f86..bcb015185 100644 --- a/packages/internal/src/handler/data/policy-utils.ts +++ b/packages/internal/src/handler/data/policy-utils.ts @@ -187,6 +187,40 @@ async function postProcessForRead( if (await shouldOmit(service, model, field)) { delete entityData[field]; } + + const fieldValue = entityData[field]; + + if (typeof fieldValue === 'bigint') { + // serialize BigInt with typing info + entityData[field] = { + type: 'BigInt', + data: fieldValue.toString(), + }; + } + + if (fieldValue instanceof Date) { + // serialize Date with typing info + entityData[field] = { + type: 'Date', + data: fieldValue.toISOString(), + }; + } + + if (typeof fieldValue === 'object') { + const fieldInfo = await service.resolveField(model, field); + if (fieldInfo?.type === 'Decimal') { + // serialize Decimal with typing info + entityData[field] = { + type: 'Decimal', + data: fieldValue.toString(), + }; + } else if (fieldInfo?.type === 'Bytes') { + entityData[field] = { + type: 'Bytes', + data: Array.from(fieldValue as Buffer), + }; + } + } } const injectTarget = args.select ?? args.include; @@ -596,13 +630,11 @@ export async function preprocessWritePayload( fieldData: any, parentData: any ) => { - if (fieldInfo.type !== 'String') { - return true; - } + // process @password field const pwdAttr = fieldInfo.attributes?.find( (attr) => attr.name === '@password' ); - if (pwdAttr) { + if (pwdAttr && fieldInfo.type !== 'String') { // hash password value let salt: string | number | undefined = pwdAttr.args.find( (arg) => arg.name === 'salt' @@ -616,6 +648,17 @@ export async function preprocessWritePayload( } parentData[fieldInfo.name] = hashSync(fieldData, salt); } + + // deserialize Buffer field + if (fieldInfo.type === 'Bytes' && Array.isArray(fieldData.data)) { + parentData[fieldInfo.name] = Buffer.from(fieldData.data); + } + + // deserialize BigInt field + if (fieldInfo.type === 'BigInt' && typeof fieldData === 'string') { + parentData[fieldInfo.name] = BigInt(fieldData); + } + return true; }; diff --git a/packages/internal/src/request.ts b/packages/internal/src/request.ts index 41ea2224a..59de775d0 100644 --- a/packages/internal/src/request.ts +++ b/packages/internal/src/request.ts @@ -1,3 +1,4 @@ +import Decimal from 'decimal.js'; import useSWR, { useSWRConfig } from 'swr'; import type { MutatorCallback, @@ -5,6 +6,66 @@ import type { SWRResponse, } from 'swr/dist/types'; +type BufferShape = { type: 'Buffer'; data: number[] }; +function isBuffer(value: unknown): value is BufferShape { + return ( + !!value && + (value as BufferShape).type === 'Buffer' && + Array.isArray((value as BufferShape).data) + ); +} + +type BigIntShape = { type: 'BigInt'; data: string }; +function isBigInt(value: unknown): value is BigIntShape { + return ( + !!value && + (value as BigIntShape).type === 'BigInt' && + typeof (value as BigIntShape).data === 'string' + ); +} + +type DateShape = { type: 'Date'; data: string }; +function isDate(value: unknown): value is BigIntShape { + return ( + !!value && + (value as DateShape).type === 'Date' && + typeof (value as DateShape).data === 'string' + ); +} + +type DecmalShape = { type: 'Decimal'; data: string }; +function isDecimal(value: unknown): value is DecmalShape { + return ( + !!value && + (value as DecmalShape).type === 'Decimal' && + typeof (value as DateShape).data === 'string' + ); +} + +const dataReviver = (key: string, value: unknown) => { + // Buffer + if (isBuffer(value)) { + return Buffer.from(value.data); + } + + // BigInt + if (isBigInt(value)) { + return BigInt(value.data); + } + + // Date + if (isDate(value)) { + return new Date(value.data); + } + + // Decimal + if (isDecimal(value)) { + return new Decimal(value.data); + } + + return value; +}; + const fetcher = async (url: string, options?: RequestInit) => { const res = await fetch(url, options); if (!res.ok) { @@ -15,7 +76,15 @@ const fetcher = async (url: string, options?: RequestInit) => { error.status = res.status; throw error; } - return res.json(); + + const textResult = await res.text(); + console.log; + try { + return JSON.parse(textResult, dataReviver); + } catch (err) { + console.error(`Unable to deserialize data:`, textResult); + throw err; + } }; function makeUrl(url: string, args: unknown) { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index f27c34861..bec0d68b1 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.11", + "version": "0.2.12", "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 c104fa434..1af0273c5 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.11", + "version": "0.2.12", "author": { "name": "ZenStack Team" }, diff --git a/packages/schema/src/res/stdlib.zmodel b/packages/schema/src/res/stdlib.zmodel index ff248b199..c08141ba2 100644 --- a/packages/schema/src/res/stdlib.zmodel +++ b/packages/schema/src/res/stdlib.zmodel @@ -10,33 +10,33 @@ enum ReferentialAction { } /* - * Reads value from an environment variable + * Reads value from an environment variable. */ function env(name: String): String {} /* - * Gets thec current login user + * Gets thec current login user. */ function auth(): Any {} /* - * Gets current date-time (as DateTime type) + * Gets current date-time (as DateTime type). */ function now(): DateTime {} /* - * Generate a globally unique identifier based on the UUID spec + * Generates a globally unique identifier based on the UUID specs. */ function uuid(): String {} /* - * Generate a globally unique identifier based on the CUID spec + * Generates a globally unique identifier based on the CUID spec. */ function cuid(): String {} /* - * Create a sequence of integers in the underlying database and assign the incremented - * values to the ID values of the created records based on the sequence + * Creates a sequence of integers in the underlying database and assign the incremented + * values to the ID values of the created records based on the sequence. */ function autoincrement(): Int {} @@ -46,57 +46,57 @@ function autoincrement(): Int {} function dbgenerated(expr: String): Any {} /* - * Defines an ID on the model + * Defines an ID on the model. */ attribute @id(map: String?) /* - * Defines a default value for a field + * Defines a default value for a field. */ attribute @default(_ value: ContextType) /* - * Defines a unique constraint for this field + * Defines a unique constraint for this field. */ attribute @unique(map: String?) /* - * Defines a compound unique constraint for the specified fields + * Defines a compound unique constraint for the specified fields. */ attribute @@unique(_ fields: FieldReference[], name: String?, map: String?) /* - * Defines an index in the database + * Defines an index in the database. */ attribute @@index(_ fields: FieldReference[], map: String?) /* - * Defines meta information about the relation + * Defines meta information about the relation. */ attribute @relation(_ name: String?, fields: FieldReference[]?, references: FieldReference[]?, onDelete: ReferentialAction?, onUpdate: ReferentialAction?, map: String?) /* - * Maps a field name or enum value from the schema to a column with a different name in the database + * Maps a field name or enum value from the schema to a column with a different name in the database. */ attribute @map(_ name: String) /* - * Maps the schema model name to a table with a different name, or an enum name to a different underlying enum in the database + * Maps the schema model name to a table with a different name, or an enum name to a different underlying enum in the database. */ attribute @@map(_ name: String) /* - * Automatically stores the time when a record was last updated + * Automatically stores the time when a record was last updated. */ attribute @updatedAt() /* - * Defines an access policy that allows a set of operations when the given condition is true + * Defines an access policy that allows a set of operations when the given condition is true. */ attribute @@allow(_ operation: String, _ condition: Boolean) /* - * Defines an access policy that denies a set of operations when the given condition is true + * Defines an access policy that denies a set of operations when the given condition is true. */ attribute @@deny(_ operation: String, _ condition: Boolean) @@ -113,6 +113,6 @@ attribute @@deny(_ operation: String, _ condition: Boolean) attribute @password(saltLength: Int?, salt: String?) /* - * Indicates that the field should be omitted when read from the generated services + * Indicates that the field should be omitted when read from the generated services. */ attribute @omit() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d41794e3e..65c3a4d7d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,10 +15,11 @@ importers: bcryptjs: ^2.4.3 colors: ^1.4.0 cuid: ^2.1.8 + decimal.js: ^10.4.2 deepcopy: ^2.1.0 eslint: ^8.27.0 jest: ^29.0.3 - next: 12.3.1 + next: ^12.3.1 react: ^17.0.2 || ^18 react-dom: ^17.0.2 || ^18 swr: ^1.3.0 @@ -31,8 +32,9 @@ importers: bcryptjs: 2.4.3 colors: 1.4.0 cuid: 2.1.8 + decimal.js: 10.4.2 deepcopy: 2.1.0 - next: 12.3.1_qtpcxnaaarbm4ws7ughq6oxfve + next: 12.3.1_6tziyx3dehkoeijunclpkpolha react: 18.2.0 react-dom: 18.2.0_react@18.2.0 swr: 1.3.0_react@18.2.0 @@ -212,7 +214,6 @@ packages: semver: 6.3.0 transitivePeerDependencies: - supports-color - dev: true /@babel/core/7.19.6: resolution: {integrity: sha512-D2Ue4KHpc6Ys2+AxpIx1BZ8+UegLLLE2p3KJEuJRKmokHOtl49jQ5ny1773KsGLZs8MQvBidAF6yWUJxRqtKtg==} @@ -235,6 +236,7 @@ packages: semver: 6.3.0 transitivePeerDependencies: - supports-color + dev: true /@babel/generator/7.19.5: resolution: {integrity: sha512-DxbNz9Lz4aMZ99qPpO1raTbcrI1ZeYh+9NR9qhfkQIbFtVEqotHojEBxHzmxhVONkGt6VyrqVQcgpefMy9pqcg==} @@ -243,7 +245,6 @@ 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==} @@ -264,7 +265,6 @@ 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==} @@ -277,6 +277,7 @@ 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==} @@ -315,7 +316,6 @@ 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==} @@ -331,6 +331,7 @@ packages: '@babel/types': 7.19.4 transitivePeerDependencies: - supports-color + dev: true /@babel/helper-plugin-utils/7.19.0: resolution: {integrity: sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw==} @@ -342,13 +343,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==} @@ -392,7 +393,6 @@ packages: hasBin: true dependencies: '@babel/types': 7.19.4 - dev: true /@babel/parser/7.19.6: resolution: {integrity: sha512-h1IUp81s2JYJ3mRkdxJgs4UvmSsRvDrx5ICSJbPvtWYv5i1nTBGcBpnog+89rAFMwvvru6E5NUHdBe01UeSzYA==} @@ -683,7 +683,6 @@ packages: globals: 11.12.0 transitivePeerDependencies: - supports-color - dev: true /@babel/traverse/7.19.6: resolution: {integrity: sha512-6l5HrUCzFM04mfbG09AagtYyR2P0B71B1wN7PfSPiksDPz2k5H9CBC1tcZpz2M8OxbKTPccByoOJ22rUKbpmQQ==} @@ -2715,6 +2714,10 @@ packages: dependencies: ms: 2.1.2 + /decimal.js/10.4.2: + resolution: {integrity: sha512-ic1yEvwT6GuvaYwBLLY6/aFFgjZdySKTE8en/fkU3QICTmRtgtSlFn0u0BXN06InZwtfCelR7j8LRiDI/02iGA==} + dev: false + /decompress-response/6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -5062,6 +5065,51 @@ packages: engines: {node: '>=10'} dev: true + /next/12.3.1_6tziyx3dehkoeijunclpkpolha: + resolution: {integrity: sha512-l7bvmSeIwX5lp07WtIiP9u2ytZMv7jIeB8iacR28PuUEFG5j0HGAPnMqyG5kbZNBG2H7tRsrQ4HCjuMOPnANZw==} + engines: {node: '>=12.22.0'} + hasBin: true + peerDependencies: + fibers: '>= 3.1.0' + node-sass: ^6.0.0 || ^7.0.0 + react: ^17.0.2 || ^18.0.0-0 + react-dom: ^17.0.2 || ^18.0.0-0 + sass: ^1.3.0 + peerDependenciesMeta: + fibers: + optional: true + node-sass: + optional: true + sass: + optional: true + dependencies: + '@next/env': 12.3.1 + '@swc/helpers': 0.4.11 + caniuse-lite: 1.0.30001409 + postcss: 8.4.14 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + styled-jsx: 5.0.7_b6k74wvxdvqypha4emuv7fd2ke + use-sync-external-store: 1.2.0_react@18.2.0 + optionalDependencies: + '@next/swc-android-arm-eabi': 12.3.1 + '@next/swc-android-arm64': 12.3.1 + '@next/swc-darwin-arm64': 12.3.1 + '@next/swc-darwin-x64': 12.3.1 + '@next/swc-freebsd-x64': 12.3.1 + '@next/swc-linux-arm-gnueabihf': 12.3.1 + '@next/swc-linux-arm64-gnu': 12.3.1 + '@next/swc-linux-arm64-musl': 12.3.1 + '@next/swc-linux-x64-gnu': 12.3.1 + '@next/swc-linux-x64-musl': 12.3.1 + '@next/swc-win32-arm64-msvc': 12.3.1 + '@next/swc-win32-ia32-msvc': 12.3.1 + '@next/swc-win32-x64-msvc': 12.3.1 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + dev: false + /next/12.3.1_qtpcxnaaarbm4ws7ughq6oxfve: resolution: {integrity: sha512-l7bvmSeIwX5lp07WtIiP9u2ytZMv7jIeB8iacR28PuUEFG5j0HGAPnMqyG5kbZNBG2H7tRsrQ4HCjuMOPnANZw==} engines: {node: '>=12.22.0'} @@ -5105,6 +5153,7 @@ packages: transitivePeerDependencies: - '@babel/core' - babel-plugin-macros + dev: true /no-case/3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} @@ -5921,6 +5970,23 @@ packages: engines: {node: '>=8'} dev: true + /styled-jsx/5.0.7_b6k74wvxdvqypha4emuv7fd2ke: + resolution: {integrity: sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + dependencies: + '@babel/core': 7.19.3 + react: 18.2.0 + dev: false + /styled-jsx/5.0.7_otspjrsspon4ofp37rshhlhp2y: resolution: {integrity: sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA==} engines: {node: '>= 12.0.0'} @@ -5936,6 +6002,7 @@ packages: dependencies: '@babel/core': 7.19.6 react: 18.2.0 + dev: true /superagent/8.0.2: resolution: {integrity: sha512-QtYZ9uaNAMexI7XWl2vAXAh0j4q9H7T0WVEI/y5qaUB3QLwxo+voUgCQ217AokJzUTIVOp0RTo7fhZrwhD7A2Q==} diff --git a/samples/todo/package-lock.json b/samples/todo/package-lock.json index c786f3797..73dd94718 100644 --- a/samples/todo/package-lock.json +++ b/samples/todo/package-lock.json @@ -1,12 +1,12 @@ { "name": "todo", - "version": "0.2.11", + "version": "0.2.12", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "todo", - "version": "0.2.11", + "version": "0.2.12", "dependencies": { "@heroicons/react": "^2.0.12", "@prisma/client": "^4.4.0", diff --git a/samples/todo/package.json b/samples/todo/package.json index f6d3e5443..9efdf340d 100644 --- a/samples/todo/package.json +++ b/samples/todo/package.json @@ -1,6 +1,6 @@ { "name": "todo", - "version": "0.2.11", + "version": "0.2.12", "private": true, "scripts": { "dev": "next dev", diff --git a/tests/integration/tests/omit.zmodel b/tests/integration/tests/omit.zmodel index 0ced7342d..01677619e 100644 --- a/tests/integration/tests/omit.zmodel +++ b/tests/integration/tests/omit.zmodel @@ -1,6 +1,6 @@ datasource db { provider = 'sqlite' - url = 'file:./password.db' + url = 'file:./omit.db' } model User { diff --git a/tests/integration/tests/type-coverage.test.ts b/tests/integration/tests/type-coverage.test.ts new file mode 100644 index 000000000..c9dc2c944 --- /dev/null +++ b/tests/integration/tests/type-coverage.test.ts @@ -0,0 +1,70 @@ +import path from 'path'; +import { makeClient, run, setup } from './utils'; +import { ServerErrorCode } from '../../../packages/internal/src/types'; + +describe('Type Coverage Tests', () => { + let origDir: string; + + beforeAll(async () => { + origDir = path.resolve('.'); + await setup('./tests/type-coverage.zmodel'); + }); + + beforeEach(() => { + run('npx prisma migrate reset --schema ./zenstack/schema.prisma -f'); + }); + + afterAll(() => { + process.chdir(origDir); + }); + + it('all types', async () => { + const data = { + string: 'string', + int: 100, + bigInt: 9007199254740991, + date: new Date(), + float: 1.23, + decimal: 1.2345, + boolean: true, + bytes: Buffer.from('hello'), + }; + await makeClient('/api/data/Foo') + .post('/') + .send({ + data, + }) + .expect(201) + .expect((resp) => { + console.log(resp.body); + + expect(resp.body.bigInt).toEqual( + expect.objectContaining({ + type: 'BigInt', + data: data.bigInt.toString(), + }) + ); + + expect(resp.body.date).toEqual( + expect.objectContaining({ + type: 'Date', + data: data.date.toISOString(), + }) + ); + + expect(resp.body.decimal).toEqual( + expect.objectContaining({ + type: 'Decimal', + data: data.decimal.toString(), + }) + ); + + expect(resp.body.bytes).toEqual( + expect.objectContaining({ + type: 'Bytes', + data: Array.from(data.bytes), + }) + ); + }); + }); +}); diff --git a/tests/integration/tests/type-coverage.zmodel b/tests/integration/tests/type-coverage.zmodel new file mode 100644 index 000000000..2d44a8842 --- /dev/null +++ b/tests/integration/tests/type-coverage.zmodel @@ -0,0 +1,19 @@ +datasource db { + provider = 'sqlite' + url = 'file:./type-coverage.db' +} + +model Foo { + id String @id @default(cuid()) + + string String + int Int + bigInt BigInt + date DateTime + float Float + decimal Decimal + boolean Boolean + bytes Bytes + + @@allow('all', true) +}