From ea227cbaccc6fcfe3d939227ce7bb1797fdc4c94 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sun, 14 Apr 2024 22:17:21 +0800 Subject: [PATCH 1/4] fix(sdk): exclude prisma module from barrel file to avoid eager loading of prisma packages - Solves the problem that launching language server triggers loading "@prisma/internals" - Removes several old logic for detecting Prisma versions --- .../plugins/openapi/src/generator-base.ts | 10 ++-------- .../plugins/openapi/src/rest-generator.ts | 2 +- packages/plugins/openapi/src/rpc-generator.ts | 3 ++- packages/plugins/swr/src/generator.ts | 12 ++--------- .../plugins/tanstack-query/src/generator.ts | 12 ++--------- packages/plugins/trpc/src/generator.ts | 3 +-- packages/plugins/trpc/src/helpers.ts | 3 ++- packages/schema/src/cli/plugin-runner.ts | 2 +- .../src/plugins/enhancer/enhance/index.ts | 4 +--- .../enhancer/policy/policy-guard-generator.ts | 2 +- packages/schema/src/plugins/prisma/index.ts | 3 ++- .../src/plugins/prisma/schema-generator.ts | 16 +-------------- packages/schema/src/plugins/zod/generator.ts | 3 +-- .../schema/src/plugins/zod/transformer.ts | 20 +++---------------- packages/schema/src/plugins/zod/types.ts | 2 +- packages/schema/src/telemetry.ts | 2 +- .../tests/generator/prisma-builder.test.ts | 2 +- .../tests/generator/prisma-generator.test.ts | 2 +- packages/sdk/package.json | 14 +++++++++++++ .../sdk/src/dmmf-helpers/aggregate-helpers.ts | 2 +- .../sdk/src/dmmf-helpers/include-helpers.ts | 2 +- .../src/dmmf-helpers/missing-types-helper.ts | 2 +- .../sdk/src/dmmf-helpers/model-helpers.ts | 2 +- .../sdk/src/dmmf-helpers/modelArgs-helpers.ts | 2 +- .../sdk/src/dmmf-helpers/select-helpers.ts | 2 +- packages/sdk/src/dmmf-helpers/types.ts | 2 +- packages/sdk/src/index.ts | 1 - packages/sdk/src/prisma.ts | 2 ++ packages/sdk/src/types.ts | 2 -- packages/testtools/src/schema.ts | 2 +- 30 files changed, 50 insertions(+), 88 deletions(-) diff --git a/packages/plugins/openapi/src/generator-base.ts b/packages/plugins/openapi/src/generator-base.ts index 2b692dd1b..38cddf16c 100644 --- a/packages/plugins/openapi/src/generator-base.ts +++ b/packages/plugins/openapi/src/generator-base.ts @@ -1,12 +1,6 @@ -import { - PluginError, - getDataModels, - hasAttribute, - type DMMF, - type PluginOptions, - type PluginResult, -} from '@zenstackhq/sdk'; +import { PluginError, getDataModels, hasAttribute, type PluginOptions, type PluginResult } from '@zenstackhq/sdk'; import { Model } from '@zenstackhq/sdk/ast'; +import type { DMMF } from '@zenstackhq/sdk/prisma'; import type { OpenAPIV3_1 as OAPI } from 'openapi-types'; import semver from 'semver'; import { fromZodError } from 'zod-validation-error'; diff --git a/packages/plugins/openapi/src/rest-generator.ts b/packages/plugins/openapi/src/rest-generator.ts index d248145fd..90383f8f9 100644 --- a/packages/plugins/openapi/src/rest-generator.ts +++ b/packages/plugins/openapi/src/rest-generator.ts @@ -9,9 +9,9 @@ import { isRelationshipField, requireOption, resolvePath, - type DMMF, } from '@zenstackhq/sdk'; import { DataModel, DataModelField, DataModelFieldType, Enum, isDataModel, isEnum } from '@zenstackhq/sdk/ast'; +import type { DMMF } from '@zenstackhq/sdk/prisma'; import fs from 'fs'; import { lowerCaseFirst } from 'lower-case-first'; import type { OpenAPIV3_1 as OAPI } from 'openapi-types'; diff --git a/packages/plugins/openapi/src/rpc-generator.ts b/packages/plugins/openapi/src/rpc-generator.ts index cd8ef133e..cb388aae2 100644 --- a/packages/plugins/openapi/src/rpc-generator.ts +++ b/packages/plugins/openapi/src/rpc-generator.ts @@ -1,6 +1,6 @@ // Inspired by: https://github.com/omar-dulaimi/prisma-trpc-generator -import { analyzePolicies, PluginError, requireOption, resolvePath, type DMMF } from '@zenstackhq/sdk'; +import { analyzePolicies, PluginError, requireOption, resolvePath } from '@zenstackhq/sdk'; import { DataModel, isDataModel } from '@zenstackhq/sdk/ast'; import { addMissingInputObjectTypesForAggregate, @@ -10,6 +10,7 @@ import { AggregateOperationSupport, resolveAggregateOperationSupport, } from '@zenstackhq/sdk/dmmf-helpers'; +import type { DMMF } from '@zenstackhq/sdk/prisma'; import * as fs from 'fs'; import { lowerCaseFirst } from 'lower-case-first'; import type { OpenAPIV3_1 as OAPI } from 'openapi-types'; diff --git a/packages/plugins/swr/src/generator.ts b/packages/plugins/swr/src/generator.ts index ad2a51223..4ab3fb79e 100644 --- a/packages/plugins/swr/src/generator.ts +++ b/packages/plugins/swr/src/generator.ts @@ -4,17 +4,14 @@ import { ensureEmptyDir, generateModelMeta, getDataModels, - getPrismaClientImportSpec, - getPrismaVersion, requireOption, resolvePath, saveProject, - type DMMF, } from '@zenstackhq/sdk'; import { DataModel, Model } from '@zenstackhq/sdk/ast'; +import { getPrismaClientImportSpec, type DMMF } from '@zenstackhq/sdk/prisma'; import { paramCase } from 'change-case'; import path from 'path'; -import semver from 'semver'; import type { OptionalKind, ParameterDeclarationStructure, Project, SourceFile } from 'ts-morph'; import { upperCaseFirst } from 'upper-case-first'; import { name } from '.'; @@ -74,7 +71,6 @@ function generateModelHooks( ]); const modelNameCap = upperCaseFirst(model.name); - const prismaVersion = getPrismaVersion(); const mutationFuncs: string[] = []; @@ -168,11 +164,7 @@ function generateModelHooks( // groupBy if (mapping.groupBy) { - let useName = modelNameCap; - if (prismaVersion && semver.gte(prismaVersion, '5.0.0')) { - // prisma 4 and 5 different typing for "groupBy" and we have to deal with it separately - useName = model.name; - } + const useName = model.name; const typeParameters = [ `T extends Prisma.${useName}GroupByArgs`, `HasSelectOrTake extends Prisma.Or>, Prisma.Extends<'take', Prisma.Keys>>`, diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index 4a2e0cd8b..7666cb742 100644 --- a/packages/plugins/tanstack-query/src/generator.ts +++ b/packages/plugins/tanstack-query/src/generator.ts @@ -5,18 +5,15 @@ import { ensureEmptyDir, generateModelMeta, getDataModels, - getPrismaClientImportSpec, - getPrismaVersion, requireOption, resolvePath, saveProject, - type DMMF, } from '@zenstackhq/sdk'; import { DataModel, Model } from '@zenstackhq/sdk/ast'; +import { getPrismaClientImportSpec, type DMMF } from '@zenstackhq/sdk/prisma'; import { paramCase } from 'change-case'; import { lowerCaseFirst } from 'lower-case-first'; import path from 'path'; -import semver from 'semver'; import { Project, SourceFile, VariableDeclarationKind } from 'ts-morph'; import { match } from 'ts-pattern'; import { upperCaseFirst } from 'upper-case-first'; @@ -274,7 +271,6 @@ function generateModelHooks( options: PluginOptions ) { const modelNameCap = upperCaseFirst(model.name); - const prismaVersion = getPrismaVersion(); const fileName = paramCase(model.name); const sf = project.createSourceFile(path.join(outDir, `${fileName}.ts`), undefined, { overwrite: true }); @@ -401,11 +397,7 @@ function generateModelHooks( // groupBy if (mapping.groupBy) { - let useName = modelNameCap; - // prisma 4 and 5 different typing for "groupBy" and we have to deal with it separately - if (prismaVersion && semver.gte(prismaVersion, '5.0.0')) { - useName = model.name; - } + const useName = model.name; const returnType = `{} extends InputErrors ? Array & diff --git a/packages/plugins/trpc/src/generator.ts b/packages/plugins/trpc/src/generator.ts index cbeb8f191..9d2cc5724 100644 --- a/packages/plugins/trpc/src/generator.ts +++ b/packages/plugins/trpc/src/generator.ts @@ -3,15 +3,14 @@ import { PluginError, RUNTIME_PACKAGE, ensureEmptyDir, - getPrismaClientImportSpec, parseOptionAsStrings, requireOption, resolvePath, saveProject, - type DMMF, type PluginOptions, } from '@zenstackhq/sdk'; import { Model } from '@zenstackhq/sdk/ast'; +import { getPrismaClientImportSpec, type DMMF } from '@zenstackhq/sdk/prisma'; import fs from 'fs'; import { lowerCaseFirst } from 'lower-case-first'; import path from 'path'; diff --git a/packages/plugins/trpc/src/helpers.ts b/packages/plugins/trpc/src/helpers.ts index 4b0030c1c..947b96b98 100644 --- a/packages/plugins/trpc/src/helpers.ts +++ b/packages/plugins/trpc/src/helpers.ts @@ -1,4 +1,5 @@ -import { PluginError, getPrismaClientImportSpec, type DMMF, type PluginOptions } from '@zenstackhq/sdk'; +import { PluginError, type PluginOptions } from '@zenstackhq/sdk'; +import { getPrismaClientImportSpec, type DMMF } from '@zenstackhq/sdk/prisma'; import { lowerCaseFirst } from 'lower-case-first'; import { CodeBlockWriter, SourceFile } from 'ts-morph'; import { upperCaseFirst } from 'upper-case-first'; diff --git a/packages/schema/src/cli/plugin-runner.ts b/packages/schema/src/cli/plugin-runner.ts index 48a4d615c..7b725cc28 100644 --- a/packages/schema/src/cli/plugin-runner.ts +++ b/packages/schema/src/cli/plugin-runner.ts @@ -11,12 +11,12 @@ import { PluginError, resolvePath, saveProject, - type DMMF, type OptionValue, type PluginDeclaredOptions, type PluginFunction, type PluginResult, } from '@zenstackhq/sdk'; +import { type DMMF } from '@zenstackhq/sdk/prisma'; import colors from 'colors'; import ora from 'ora'; import path from 'path'; diff --git a/packages/schema/src/plugins/enhancer/enhance/index.ts b/packages/schema/src/plugins/enhancer/enhance/index.ts index 0c630e28c..cf75c3315 100644 --- a/packages/schema/src/plugins/enhancer/enhance/index.ts +++ b/packages/schema/src/plugins/enhancer/enhance/index.ts @@ -4,11 +4,8 @@ import { getAttribute, getAttributeArg, getAuthModel, - getDMMF, getDataModels, - getPrismaClientImportSpec, isDelegateModel, - type DMMF, type PluginOptions, } from '@zenstackhq/sdk'; import { @@ -20,6 +17,7 @@ import { isReferenceExpr, type Model, } from '@zenstackhq/sdk/ast'; +import { getDMMF, getPrismaClientImportSpec, type DMMF } from '@zenstackhq/sdk/prisma'; import fs from 'fs'; import path from 'path'; import { diff --git a/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts index e438f291c..753ef8f19 100644 --- a/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts +++ b/packages/schema/src/plugins/enhancer/policy/policy-guard-generator.ts @@ -37,7 +37,6 @@ import { getDataModels, getIdFields, getLiteral, - getPrismaClientImportSpec, hasAttribute, hasValidationAttributes, isAuthInvocation, @@ -47,6 +46,7 @@ import { isFutureExpr, resolved, } from '@zenstackhq/sdk'; +import { getPrismaClientImportSpec } from '@zenstackhq/sdk/prisma'; import { streamAllContents, streamAst, streamContents } from 'langium'; import { lowerCaseFirst } from 'lower-case-first'; import path from 'path'; diff --git a/packages/schema/src/plugins/prisma/index.ts b/packages/schema/src/plugins/prisma/index.ts index 478b6a54b..b016d17f9 100644 --- a/packages/schema/src/plugins/prisma/index.ts +++ b/packages/schema/src/plugins/prisma/index.ts @@ -1,5 +1,6 @@ -import { PluginError, PluginFunction, getDMMF, getLiteral, resolvePath } from '@zenstackhq/sdk'; +import { PluginError, PluginFunction, getLiteral, resolvePath } from '@zenstackhq/sdk'; import { GeneratorDecl, isGeneratorDecl } from '@zenstackhq/sdk/ast'; +import { getDMMF } from '@zenstackhq/sdk/prisma'; import fs from 'fs'; import path from 'path'; import stripColor from 'strip-color'; diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 1e5cdb318..d7e7f5b9a 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -28,6 +28,7 @@ import { NumberLiteral, StringLiteral, } from '@zenstackhq/language/ast'; +import { getPrismaVersion } from '@zenstackhq/sdk/prisma'; import { match, P } from 'ts-pattern'; import { getIdFields } from '../../utils/ast-utils'; @@ -37,7 +38,6 @@ import { getAttributeArg, getAttributeArgLiteral, getLiteral, - getPrismaVersion, isDelegateModel, isIdField, PluginError, @@ -224,20 +224,6 @@ export class PrismaSchemaGenerator { throw new PluginError(name, 'option "previewFeatures" must be an array'); } - if (semver.lt(prismaVersion, '5.0.0')) { - // extendedWhereUnique feature is opt-in pre V5 - if (!previewFeatures.includes('extendedWhereUnique')) { - previewFeatures.push('extendedWhereUnique'); - } - } - - if (semver.lt(prismaVersion, '5.0.0')) { - // fieldReference feature is opt-in pre V5 - if (!previewFeatures.includes('fieldReference')) { - previewFeatures.push('fieldReference'); - } - } - if (previewFeatures.length > 0) { const curr = generator.fields.find((f) => f.name === 'previewFeatures'); if (!curr) { diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index baacf0f96..5e542540d 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -3,17 +3,16 @@ import { PluginOptions, ensureEmptyDir, getDataModels, - getPrismaClientImportSpec, hasAttribute, isEnumFieldReference, isForeignKeyField, isFromStdlib, parseOptionAsStrings, resolvePath, - type DMMF, } from '@zenstackhq/sdk'; import { DataModel, EnumField, Model, isDataModel, isEnum } from '@zenstackhq/sdk/ast'; import { addMissingInputObjectTypes, resolveAggregateOperationSupport } from '@zenstackhq/sdk/dmmf-helpers'; +import { getPrismaClientImportSpec, type DMMF } from '@zenstackhq/sdk/prisma'; import { streamAllContents } from 'langium'; import path from 'path'; import type { SourceFile } from 'ts-morph'; diff --git a/packages/schema/src/plugins/zod/transformer.ts b/packages/schema/src/plugins/zod/transformer.ts index 5b62687ff..6d51c3ef0 100644 --- a/packages/schema/src/plugins/zod/transformer.ts +++ b/packages/schema/src/plugins/zod/transformer.ts @@ -1,14 +1,9 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { - getPrismaClientImportSpec, - getPrismaVersion, - type PluginOptions, - type DMMF as PrismaDMMF, -} from '@zenstackhq/sdk'; +import { type PluginOptions } from '@zenstackhq/sdk'; import { checkModelHasModelRelation, findModelByName, isAggregateInputType } from '@zenstackhq/sdk/dmmf-helpers'; +import { getPrismaClientImportSpec, type DMMF as PrismaDMMF } from '@zenstackhq/sdk/prisma'; import { indentString } from '@zenstackhq/sdk/utils'; import path from 'path'; -import * as semver from 'semver'; import type { Project, SourceFile } from 'ts-morph'; import { upperCaseFirst } from 'upper-case-first'; import { AggregateOperationSupport, TransformerParams } from './types'; @@ -568,10 +563,6 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; const aggregateOperations = []; - // DMMF messed up the model name casing used in the aggregate operations, - // AND the casing behavior varies from version to version -_-|| - const prismaVersion = getPrismaVersion(); - if (this.aggregateOperationSupport[modelName]?.count) { imports.push( `import { ${modelName}CountAggregateInputObjectSchema } from '../objects/${modelName}CountAggregateInput.schema'` @@ -629,12 +620,7 @@ export const ${this.name}ObjectSchema: SchemaType = ${schema} as SchemaType;`; ', ' )} }),`; - // prisma 4 and 5 different typing for "groupBy" and we have to deal with it separately - if (prismaVersion && semver.gte(prismaVersion, '5.0.0')) { - operations.push(['groupBy', origModelName]); - } else { - operations.push(['groupBy', modelName]); - } + operations.push(['groupBy', origModelName]); } // count diff --git a/packages/schema/src/plugins/zod/types.ts b/packages/schema/src/plugins/zod/types.ts index a33706e4f..b64995448 100644 --- a/packages/schema/src/plugins/zod/types.ts +++ b/packages/schema/src/plugins/zod/types.ts @@ -1,4 +1,4 @@ -import type { DMMF as PrismaDMMF } from '@zenstackhq/sdk'; +import type { DMMF as PrismaDMMF } from '@zenstackhq/sdk/prisma'; import { Project } from 'ts-morph'; export type TransformerParams = { diff --git a/packages/schema/src/telemetry.ts b/packages/schema/src/telemetry.ts index 45983886d..9ecbb4672 100644 --- a/packages/schema/src/telemetry.ts +++ b/packages/schema/src/telemetry.ts @@ -1,5 +1,5 @@ import { createId } from '@paralleldrive/cuid2'; -import { getPrismaVersion } from '@zenstackhq/sdk'; +import { getPrismaVersion } from '@zenstackhq/sdk/prisma'; import exitHook from 'async-exit-hook'; import { CommanderError } from 'commander'; import { init, Mixpanel } from 'mixpanel'; diff --git a/packages/schema/tests/generator/prisma-builder.test.ts b/packages/schema/tests/generator/prisma-builder.test.ts index a3944401c..8144110de 100644 --- a/packages/schema/tests/generator/prisma-builder.test.ts +++ b/packages/schema/tests/generator/prisma-builder.test.ts @@ -1,4 +1,4 @@ -import { getDMMF } from '@zenstackhq/sdk'; +import { getDMMF } from '@zenstackhq/sdk/prisma'; import { AttributeArg, AttributeArgValue, diff --git a/packages/schema/tests/generator/prisma-generator.test.ts b/packages/schema/tests/generator/prisma-generator.test.ts index 5e629bc2a..c91034d1d 100644 --- a/packages/schema/tests/generator/prisma-generator.test.ts +++ b/packages/schema/tests/generator/prisma-generator.test.ts @@ -1,6 +1,6 @@ /// -import { getDMMF } from '@zenstackhq/sdk'; +import { getDMMF } from '@zenstackhq/sdk/prisma'; import fs from 'fs'; import path from 'path'; import tmp from 'tmp'; diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 6a7d24fce..35f3ed995 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -31,5 +31,19 @@ }, "devDependencies": { "@types/semver": "^7.3.13" + }, + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + }, + "./ast": { + "types": "./ast.d.ts", + "default": "./ast.js" + }, + "./prisma": { + "types": "./prisma.d.ts", + "default": "./prisma.js" + } } } diff --git a/packages/sdk/src/dmmf-helpers/aggregate-helpers.ts b/packages/sdk/src/dmmf-helpers/aggregate-helpers.ts index b53f34260..bec9632a1 100644 --- a/packages/sdk/src/dmmf-helpers/aggregate-helpers.ts +++ b/packages/sdk/src/dmmf-helpers/aggregate-helpers.ts @@ -1,5 +1,5 @@ import { upperCaseFirst } from 'upper-case-first'; -import type { DMMF } from '../types'; +import type { DMMF } from '../prisma'; import { AggregateOperationSupport } from './types'; const isAggregateOutputType = (name: string) => /(?:Count|Avg|Sum|Min|Max)AggregateOutputType$/.test(name); diff --git a/packages/sdk/src/dmmf-helpers/include-helpers.ts b/packages/sdk/src/dmmf-helpers/include-helpers.ts index 0699bca9a..c09c72426 100644 --- a/packages/sdk/src/dmmf-helpers/include-helpers.ts +++ b/packages/sdk/src/dmmf-helpers/include-helpers.ts @@ -1,4 +1,4 @@ -import type { DMMF } from '../types'; +import type { DMMF } from '../prisma'; import { checkIsModelRelationField, checkModelHasManyModelRelation, checkModelHasModelRelation } from './model-helpers'; export function addMissingInputObjectTypesForInclude(inputObjectTypes: DMMF.InputType[], models: DMMF.Model[]) { diff --git a/packages/sdk/src/dmmf-helpers/missing-types-helper.ts b/packages/sdk/src/dmmf-helpers/missing-types-helper.ts index 4e83b8590..dcdff8684 100644 --- a/packages/sdk/src/dmmf-helpers/missing-types-helper.ts +++ b/packages/sdk/src/dmmf-helpers/missing-types-helper.ts @@ -1,4 +1,4 @@ -import type { DMMF } from '../types'; +import type { DMMF } from '../prisma'; import { addMissingInputObjectTypesForAggregate } from './aggregate-helpers'; import { addMissingInputObjectTypesForInclude } from './include-helpers'; import { addMissingInputObjectTypesForModelArgs } from './modelArgs-helpers'; diff --git a/packages/sdk/src/dmmf-helpers/model-helpers.ts b/packages/sdk/src/dmmf-helpers/model-helpers.ts index f249d7c20..62bbd9980 100644 --- a/packages/sdk/src/dmmf-helpers/model-helpers.ts +++ b/packages/sdk/src/dmmf-helpers/model-helpers.ts @@ -1,4 +1,4 @@ -import type { DMMF } from '../types'; +import type { DMMF } from '../prisma'; export function checkModelHasModelRelation(model: DMMF.Model) { const { fields: modelFields } = model; diff --git a/packages/sdk/src/dmmf-helpers/modelArgs-helpers.ts b/packages/sdk/src/dmmf-helpers/modelArgs-helpers.ts index 444f2ce90..79b3a9f98 100644 --- a/packages/sdk/src/dmmf-helpers/modelArgs-helpers.ts +++ b/packages/sdk/src/dmmf-helpers/modelArgs-helpers.ts @@ -1,4 +1,4 @@ -import type { DMMF } from '../types'; +import type { DMMF } from '../prisma'; import { checkModelHasModelRelation } from './model-helpers'; export function addMissingInputObjectTypesForModelArgs(inputObjectTypes: DMMF.InputType[], models: DMMF.Model[]) { diff --git a/packages/sdk/src/dmmf-helpers/select-helpers.ts b/packages/sdk/src/dmmf-helpers/select-helpers.ts index 74b13de55..6037eecd8 100644 --- a/packages/sdk/src/dmmf-helpers/select-helpers.ts +++ b/packages/sdk/src/dmmf-helpers/select-helpers.ts @@ -1,4 +1,4 @@ -import type { DMMF } from '../types'; +import type { DMMF } from '../prisma'; import { checkIsModelRelationField, checkModelHasManyModelRelation } from './model-helpers'; export function addMissingInputObjectTypesForSelect( diff --git a/packages/sdk/src/dmmf-helpers/types.ts b/packages/sdk/src/dmmf-helpers/types.ts index 83858ba02..a1fbc6c59 100644 --- a/packages/sdk/src/dmmf-helpers/types.ts +++ b/packages/sdk/src/dmmf-helpers/types.ts @@ -1,4 +1,4 @@ -import type { DMMF } from '../types'; +import type { DMMF } from '../prisma'; export type TransformerParams = { enumTypes?: DMMF.SchemaEnum[]; diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 5013267e8..3c89805d9 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -2,7 +2,6 @@ export * from './code-gen'; export * from './constants'; export { generate as generateModelMeta } from './model-meta-generator'; export * from './policy'; -export * from './prisma'; export * from './types'; export * from './typescript-expression-transformer'; export * from './utils'; diff --git a/packages/sdk/src/prisma.ts b/packages/sdk/src/prisma.ts index e3e540654..ef642f014 100644 --- a/packages/sdk/src/prisma.ts +++ b/packages/sdk/src/prisma.ts @@ -70,3 +70,5 @@ export function getPrismaVersion(): string | undefined { } } } + +export type { DMMF } from '@prisma/generator-helper'; diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 7ba72807d..a6a4b8629 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -93,5 +93,3 @@ export class PluginError extends Error { super(message); } } - -export type { DMMF } from '@prisma/generator-helper'; diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index 9e19ab1b6..76b2d83da 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -8,7 +8,7 @@ import { type EnhancementKind, type EnhancementOptions, } from '@zenstackhq/runtime'; -import { getDMMF, type DMMF } from '@zenstackhq/sdk'; +import { getDMMF, type DMMF } from '@zenstackhq/sdk/prisma'; import { execSync } from 'child_process'; import * as fs from 'fs'; import json from 'json5'; From eb75ac5772303641d148af7570c92e7fbd7b9a7f Mon Sep 17 00:00:00 2001 From: JG Date: Sun, 14 Apr 2024 22:55:41 +0800 Subject: [PATCH 2/4] feat: support prisma format indentation --- packages/schema/schema.zmodel | 225 ++++++++++++++++++ packages/schema/src/cli/actions/format.ts | 11 +- packages/schema/src/cli/cli-util.ts | 4 +- packages/schema/src/cli/index.ts | 1 + .../src/language-server/zmodel-formatter.ts | 54 ++++- .../schema/tests/schema/formatter.test.ts | 2 +- tests/integration/tests/cli/format.test.ts | 26 ++ 7 files changed, 313 insertions(+), 10 deletions(-) create mode 100644 packages/schema/schema.zmodel diff --git a/packages/schema/schema.zmodel b/packages/schema/schema.zmodel new file mode 100644 index 000000000..fdf55b389 --- /dev/null +++ b/packages/schema/schema.zmodel @@ -0,0 +1,225 @@ +/* +* Sample model for a collaborative Todo app +*/ + +/* + * Data source definition + */ +datasource db { + provider = 'postgresql' + url = env('DATABASE_URL') +} + +generator js { + provider = 'prisma-client-js' +} + +plugin hooks { + provider = '@zenstackhq/swr' + output = 'lib/hooks' +} + +/* + * Enum for user's role in a space + */ +enum SpaceUserRole { + USER + ADMIN +} + +/* + * Model for a space in which users can collaborate on Lists and Todos + */ +model Space { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + name String @length(4, 50) + slug String @unique @regex('^[0-9a-zA-Z]{4,16}$') + members SpaceUser[] + + testMemebers User[] + @@allow('create', testMemebers?[id == auth().id]) + + lists List[] + + // require login + @@deny('all', auth() == null) + + // everyone can create a space + + + // any user in the space can read the space + @@allow('read', members?[user == auth()]) + + // space admin can update and delete + @@allow('update,delete', members?[user == auth() && role == ADMIN] ) +} + +/* + * Model representing membership of a user in a space + */ +model SpaceUser { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) + spaceId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String + role SpaceUserRole + @@unique([userId, spaceId]) + + // require login + @@deny('all', auth() == null) + + // space admin can create/update/delete + @@allow('create,update,delete', space.members?[user == auth() && this.role == ADMIN]) + + // user can read entries for spaces which he's a member of + @@allow('read', space.members?[this.role == "bac"]) +} + +/* + * Model for a user + */ +model User { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + email String @unique @email + emailVerified DateTime? + password String? @password @omit + name String? + spaces SpaceUser[] + image String? @url + lists List[] + todos Todo[] + + // next-auth + accounts Account[] + + // can be created by anyone, even not logged in + @@allow('create', true) + + // can be read by users sharing any space + @@allow('read', spaces?[space.members?[this == auth()]]) + + // full access by oneself + @@allow('all', auth() == this) + + space Space @relation(fields: [spaceId], references: [id]) + spaceId String +} + +/* + * Model for a Todo list + */ +model List { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) + spaceId String + owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) + ownerId String + title String @length(1, 100) + private Boolean @default(false) + todos Todo[] + + // require login + @@deny('all', auth() == null) + + // can be read by owner or space members (only if not private) + @@allow('read', owner == auth() || (space.members?[this.title == "sdfd"] && !private)) + + // when create, owner must be set to current user, and user must be in the space + @@allow('create', owner == auth() && space.members?[user == auth()]) + + // when create, owner must be set to current user, and user must be in the space + // update is not allowed to change owner + @@allow('update', owner == auth() && space.members?[user == auth()] && future().owner == owner) + + // can be deleted by owner + @@allow('delete', owner == auth()) +} + +/* + * Model for a single Todo + */ +model Todo { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) + ownerId String + list List @relation(fields: [listId], references: [id], onDelete: Cascade) + listId String + title String @length(1, 100) + completedAt DateTime? + + // require login + @@deny('all', auth() == null) + + // owner has full access, also space members have full access (if the parent List is not private) + @@allow('all', list.owner == auth()) + @@allow('all', list.space.members?[user == auth()] && !list.private) + + // update cannot change owner + @@deny('update', future().owner != owner) +} + +// next-auth +model Account { + id String @id @default(uuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? + refresh_token_expires_in Int? + access_token String? + expires_at Int? + token_type String? + scope String? + id_token String? + session_state String? + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + @@unique([provider, providerAccountId]) +} + + + + +abstract model Base { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? @omit + + @@deny('read', deletedAt != null) + @@deny('delete', true) +} + +model User extends Base { + email String @unique @db.Citext @email @trim @lower + role UserRole @default(USER) @deny('read,update', auth().role != ADMIN) + + @@allow('all', auth() == this) +} + +abstract model UserEntity extends Base { + userId String + user User @relation(fields: [userId], references: [id]) + + @@allow('create', userId == auth().id) + @@allow('update', userId == auth().id && future().userId == auth().id) +} + +abstract model PrivateUserEntity extends UserEntity { + @@allow('read', userId == auth().id) +} + +abstract model PublicUserEntity extends UserEntity { + @@allow('read', true) +} \ No newline at end of file diff --git a/packages/schema/src/cli/actions/format.ts b/packages/schema/src/cli/actions/format.ts index 67f2d8a14..4d00031f9 100644 --- a/packages/schema/src/cli/actions/format.ts +++ b/packages/schema/src/cli/actions/format.ts @@ -6,10 +6,17 @@ import ora from 'ora'; import { CliError } from '../cli-error'; import { formatDocument, getDefaultSchemaLocation } from '../cli-util'; -export async function format(_projectPath: string, options: { schema: string }) { +type Options = { + schema: string; + prismaStyle?: boolean; +}; + +export async function format(_projectPath: string, options: Options) { const version = getVersion(); console.log(colors.bold(`⌛️ ZenStack CLI v${version}`)); + console.log(options.prismaStyle); + const schemaFile = options.schema ?? getDefaultSchemaLocation(); if (!fs.existsSync(schemaFile)) { console.error(colors.red(`File ${schemaFile} does not exist.`)); @@ -18,7 +25,7 @@ export async function format(_projectPath: string, options: { schema: string }) const spinner = ora(`Formatting ${schemaFile}`).start(); try { - const formattedDoc = await formatDocument(schemaFile); + const formattedDoc = await formatDocument(schemaFile, options.prismaStyle); await writeFile(schemaFile, formattedDoc); spinner.succeed(); } catch (e) { diff --git a/packages/schema/src/cli/cli-util.ts b/packages/schema/src/cli/cli-util.ts index 6c8508dc5..89d194fb9 100644 --- a/packages/schema/src/cli/cli-util.ts +++ b/packages/schema/src/cli/cli-util.ts @@ -266,7 +266,7 @@ export async function checkNewVersion() { } } -export async function formatDocument(fileName: string) { +export async function formatDocument(fileName: string, isPrismaStyle = true) { const services = createZModelServices(NodeFileSystem).ZModel; const extensions = services.LanguageMetaData.fileExtensions; if (!extensions.includes(path.extname(fileName))) { @@ -279,6 +279,8 @@ export async function formatDocument(fileName: string) { const formatter = services.lsp.Formatter as ZModelFormatter; + formatter.setPrismaStyle(isPrismaStyle); + const identifier = { uri: document.uri.toString() }; const options = formatter.getFormatOptions() ?? { insertSpaces: true, diff --git a/packages/schema/src/cli/index.ts b/packages/schema/src/cli/index.ts index a3ea38238..f1a5c4cf0 100644 --- a/packages/schema/src/cli/index.ts +++ b/packages/schema/src/cli/index.ts @@ -128,6 +128,7 @@ export function createProgram() { .command('format') .description('Format a ZenStack schema file.') .addOption(schemaOption) + .option('--no-prisma-style', 'do not use prisma style') .action(formatAction); // make sure config is loaded before actions run diff --git a/packages/schema/src/language-server/zmodel-formatter.ts b/packages/schema/src/language-server/zmodel-formatter.ts index 3e3d1f018..f4d983884 100644 --- a/packages/schema/src/language-server/zmodel-formatter.ts +++ b/packages/schema/src/language-server/zmodel-formatter.ts @@ -5,13 +5,34 @@ import { FormattingOptions, Range, TextEdit } from 'vscode-languageserver'; export class ZModelFormatter extends AbstractFormatter { private formatOptions?: FormattingOptions; + private isPrismaStyle = true; protected format(node: AstNode): void { const formatter = this.getNodeFormatter(node); - if (ast.isDataModelField(node)) { + + if (!this.isPrismaStyle && ast.isDataModelField(node)) { formatter.property('type').prepend(Formatting.oneSpace()); if (node.attributes.length > 0) { formatter.properties('attributes').prepend(Formatting.oneSpace()); } + } else if (this.isPrismaStyle && ast.isDataModel(node) && node.fields.length > 0) { + const nodes = formatter.nodes(...node.fields); + nodes.prepend(Formatting.noIndent()); + + const compareFn = (a: number, b: number) => b - a; + const maxNameLength = node.fields.map((x) => x.name.length).sort(compareFn)[0]; + const maxTypeLength = node.fields.map(this.getFieldTypeLength).sort(compareFn)[0]; + + node.fields.forEach((field) => { + const fieldFormatter = this.getNodeFormatter(field); + fieldFormatter.property('type').prepend(Formatting.spaces(maxNameLength - field.name.length + 1)); + if (field.attributes.length > 0) { + fieldFormatter + .node(field.attributes[0]) + .prepend(Formatting.spaces(maxTypeLength - this.getFieldTypeLength(field) + 1)); + + fieldFormatter.nodes(...field.attributes.slice(1)).prepend(Formatting.oneSpace()); + } + }); } else if (ast.isDataModelFieldAttribute(node)) { formatter.keyword('(').surround(Formatting.noSpace()); formatter.keyword(')').prepend(Formatting.noSpace()); @@ -22,17 +43,20 @@ export class ZModelFormatter extends AbstractFormatter { } else if (ast.isAttributeArg(node)) { formatter.keyword(':').prepend(Formatting.noSpace()); formatter.keyword(':').append(Formatting.oneSpace()); - } else if (ast.isAbstractDeclaration(node)) { + } else if (ast.isModel(node)) { + const model = node as ast.Model; + const nodes = formatter.nodes(...model.declarations); + nodes.prepend(Formatting.noIndent()); + } + + // Should always run this no matter what formatting style it uses + if (ast.isAbstractDeclaration(node)) { const bracesOpen = formatter.keyword('{'); const bracesClose = formatter.keyword('}'); // allow extra blank lines between declarations formatter.interior(bracesOpen, bracesClose).prepend(Formatting.indent({ allowMore: true })); bracesOpen.prepend(Formatting.oneSpace()); bracesClose.prepend(Formatting.newLine()); - } else if (ast.isModel(node)) { - const model = node as ast.Model; - const nodes = formatter.nodes(...model.declarations); - nodes.prepend(Formatting.noIndent()); } } @@ -52,4 +76,22 @@ export class ZModelFormatter extends AbstractFormatter { public getIndent() { return 1; } + + public setPrismaStyle(isPrismaStyle: boolean) { + this.isPrismaStyle = isPrismaStyle; + } + + private getFieldTypeLength(field: ast.DataModelField) { + let length = (field.type.type || field.type.reference?.$refText)!.length; + + if (field.type.optional) { + length += 1; + } + + if (field.type.array) { + length += 2; + } + + return length; + } } diff --git a/packages/schema/tests/schema/formatter.test.ts b/packages/schema/tests/schema/formatter.test.ts index 35b08707d..24917436d 100644 --- a/packages/schema/tests/schema/formatter.test.ts +++ b/packages/schema/tests/schema/formatter.test.ts @@ -25,7 +25,7 @@ plugin swrHooks { output = 'lib/hooks' } model User { - id String @id + id String @id name String? } enum Role { diff --git a/tests/integration/tests/cli/format.test.ts b/tests/integration/tests/cli/format.test.ts index 9d7b2a52b..8fca5b6a2 100644 --- a/tests/integration/tests/cli/format.test.ts +++ b/tests/integration/tests/cli/format.test.ts @@ -35,6 +35,32 @@ generator client { model Post { id Int @id() @default(autoincrement()) users User[] +}`; + // set up schema + fs.writeFileSync('schema.zmodel', model, 'utf-8'); + const program = createProgram(); + await program.parseAsync(['format', '--no-prisma-style'], { from: 'user' }); + + expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(formattedModel); + }); + + it('prisma format', async () => { + const model = ` + datasource db {provider="sqlite" url="file:./dev.db"} + generator client {provider = "prisma-client-js"} + model Post {id Int @id() @default(autoincrement())users User[]}`; + + const formattedModel = ` +datasource db { + provider="sqlite" + url="file:./dev.db" +} +generator client { + provider = "prisma-client-js" +} +model Post { + id Int @id() @default(autoincrement()) + users User[] }`; // set up schema fs.writeFileSync('schema.zmodel', model, 'utf-8'); From 404b45d60dbfff509def42a020e3f855a7ad759a Mon Sep 17 00:00:00 2001 From: JG Date: Mon, 15 Apr 2024 00:27:23 +0800 Subject: [PATCH 3/4] remove unused file --- packages/schema/schema.zmodel | 225 ---------------------------------- 1 file changed, 225 deletions(-) delete mode 100644 packages/schema/schema.zmodel diff --git a/packages/schema/schema.zmodel b/packages/schema/schema.zmodel deleted file mode 100644 index fdf55b389..000000000 --- a/packages/schema/schema.zmodel +++ /dev/null @@ -1,225 +0,0 @@ -/* -* Sample model for a collaborative Todo app -*/ - -/* - * Data source definition - */ -datasource db { - provider = 'postgresql' - url = env('DATABASE_URL') -} - -generator js { - provider = 'prisma-client-js' -} - -plugin hooks { - provider = '@zenstackhq/swr' - output = 'lib/hooks' -} - -/* - * Enum for user's role in a space - */ -enum SpaceUserRole { - USER - ADMIN -} - -/* - * Model for a space in which users can collaborate on Lists and Todos - */ -model Space { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - name String @length(4, 50) - slug String @unique @regex('^[0-9a-zA-Z]{4,16}$') - members SpaceUser[] - - testMemebers User[] - @@allow('create', testMemebers?[id == auth().id]) - - lists List[] - - // require login - @@deny('all', auth() == null) - - // everyone can create a space - - - // any user in the space can read the space - @@allow('read', members?[user == auth()]) - - // space admin can update and delete - @@allow('update,delete', members?[user == auth() && role == ADMIN] ) -} - -/* - * Model representing membership of a user in a space - */ -model SpaceUser { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) - spaceId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId String - role SpaceUserRole - @@unique([userId, spaceId]) - - // require login - @@deny('all', auth() == null) - - // space admin can create/update/delete - @@allow('create,update,delete', space.members?[user == auth() && this.role == ADMIN]) - - // user can read entries for spaces which he's a member of - @@allow('read', space.members?[this.role == "bac"]) -} - -/* - * Model for a user - */ -model User { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - email String @unique @email - emailVerified DateTime? - password String? @password @omit - name String? - spaces SpaceUser[] - image String? @url - lists List[] - todos Todo[] - - // next-auth - accounts Account[] - - // can be created by anyone, even not logged in - @@allow('create', true) - - // can be read by users sharing any space - @@allow('read', spaces?[space.members?[this == auth()]]) - - // full access by oneself - @@allow('all', auth() == this) - - space Space @relation(fields: [spaceId], references: [id]) - spaceId String -} - -/* - * Model for a Todo list - */ -model List { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade) - spaceId String - owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) - ownerId String - title String @length(1, 100) - private Boolean @default(false) - todos Todo[] - - // require login - @@deny('all', auth() == null) - - // can be read by owner or space members (only if not private) - @@allow('read', owner == auth() || (space.members?[this.title == "sdfd"] && !private)) - - // when create, owner must be set to current user, and user must be in the space - @@allow('create', owner == auth() && space.members?[user == auth()]) - - // when create, owner must be set to current user, and user must be in the space - // update is not allowed to change owner - @@allow('update', owner == auth() && space.members?[user == auth()] && future().owner == owner) - - // can be deleted by owner - @@allow('delete', owner == auth()) -} - -/* - * Model for a single Todo - */ -model Todo { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) - ownerId String - list List @relation(fields: [listId], references: [id], onDelete: Cascade) - listId String - title String @length(1, 100) - completedAt DateTime? - - // require login - @@deny('all', auth() == null) - - // owner has full access, also space members have full access (if the parent List is not private) - @@allow('all', list.owner == auth()) - @@allow('all', list.space.members?[user == auth()] && !list.private) - - // update cannot change owner - @@deny('update', future().owner != owner) -} - -// next-auth -model Account { - id String @id @default(uuid()) - userId String - type String - provider String - providerAccountId String - refresh_token String? - refresh_token_expires_in Int? - access_token String? - expires_at Int? - token_type String? - scope String? - id_token String? - session_state String? - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - @@unique([provider, providerAccountId]) -} - - - - -abstract model Base { - id String @id @default(uuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? @omit - - @@deny('read', deletedAt != null) - @@deny('delete', true) -} - -model User extends Base { - email String @unique @db.Citext @email @trim @lower - role UserRole @default(USER) @deny('read,update', auth().role != ADMIN) - - @@allow('all', auth() == this) -} - -abstract model UserEntity extends Base { - userId String - user User @relation(fields: [userId], references: [id]) - - @@allow('create', userId == auth().id) - @@allow('update', userId == auth().id && future().userId == auth().id) -} - -abstract model PrivateUserEntity extends UserEntity { - @@allow('read', userId == auth().id) -} - -abstract model PublicUserEntity extends UserEntity { - @@allow('read', true) -} \ No newline at end of file From 6a9937f56cd862b677f1fd04f998629572012467 Mon Sep 17 00:00:00 2001 From: JG Date: Mon, 15 Apr 2024 08:54:47 +0800 Subject: [PATCH 4/4] refactor logic --- packages/schema/src/cli/actions/format.ts | 2 - .../src/language-server/zmodel-formatter.ts | 51 +++++++++---------- 2 files changed, 23 insertions(+), 30 deletions(-) diff --git a/packages/schema/src/cli/actions/format.ts b/packages/schema/src/cli/actions/format.ts index 4d00031f9..c3d2a6bed 100644 --- a/packages/schema/src/cli/actions/format.ts +++ b/packages/schema/src/cli/actions/format.ts @@ -15,8 +15,6 @@ export async function format(_projectPath: string, options: Options) { const version = getVersion(); console.log(colors.bold(`⌛️ ZenStack CLI v${version}`)); - console.log(options.prismaStyle); - const schemaFile = options.schema ?? getDefaultSchemaLocation(); if (!fs.existsSync(schemaFile)) { console.error(colors.red(`File ${schemaFile} does not exist.`)); diff --git a/packages/schema/src/language-server/zmodel-formatter.ts b/packages/schema/src/language-server/zmodel-formatter.ts index f4d983884..1121290ec 100644 --- a/packages/schema/src/language-server/zmodel-formatter.ts +++ b/packages/schema/src/language-server/zmodel-formatter.ts @@ -9,30 +9,28 @@ export class ZModelFormatter extends AbstractFormatter { protected format(node: AstNode): void { const formatter = this.getNodeFormatter(node); - if (!this.isPrismaStyle && ast.isDataModelField(node)) { - formatter.property('type').prepend(Formatting.oneSpace()); - if (node.attributes.length > 0) { - formatter.properties('attributes').prepend(Formatting.oneSpace()); - } - } else if (this.isPrismaStyle && ast.isDataModel(node) && node.fields.length > 0) { - const nodes = formatter.nodes(...node.fields); - nodes.prepend(Formatting.noIndent()); + if (ast.isDataModelField(node)) { + if (this.isPrismaStyle && ast.isDataModel(node.$container)) { + const dataModel = node.$container; - const compareFn = (a: number, b: number) => b - a; - const maxNameLength = node.fields.map((x) => x.name.length).sort(compareFn)[0]; - const maxTypeLength = node.fields.map(this.getFieldTypeLength).sort(compareFn)[0]; + const compareFn = (a: number, b: number) => b - a; + const maxNameLength = dataModel.fields.map((x) => x.name.length).sort(compareFn)[0]; + const maxTypeLength = dataModel.fields.map(this.getFieldTypeLength).sort(compareFn)[0]; - node.fields.forEach((field) => { - const fieldFormatter = this.getNodeFormatter(field); - fieldFormatter.property('type').prepend(Formatting.spaces(maxNameLength - field.name.length + 1)); - if (field.attributes.length > 0) { - fieldFormatter - .node(field.attributes[0]) - .prepend(Formatting.spaces(maxTypeLength - this.getFieldTypeLength(field) + 1)); + formatter.property('type').prepend(Formatting.spaces(maxNameLength - node.name.length + 1)); + if (node.attributes.length > 0) { + formatter + .node(node.attributes[0]) + .prepend(Formatting.spaces(maxTypeLength - this.getFieldTypeLength(node) + 1)); - fieldFormatter.nodes(...field.attributes.slice(1)).prepend(Formatting.oneSpace()); + formatter.nodes(...node.attributes.slice(1)).prepend(Formatting.oneSpace()); } - }); + } else { + formatter.property('type').prepend(Formatting.oneSpace()); + if (node.attributes.length > 0) { + formatter.properties('attributes').prepend(Formatting.oneSpace()); + } + } } else if (ast.isDataModelFieldAttribute(node)) { formatter.keyword('(').surround(Formatting.noSpace()); formatter.keyword(')').prepend(Formatting.noSpace()); @@ -43,20 +41,17 @@ export class ZModelFormatter extends AbstractFormatter { } else if (ast.isAttributeArg(node)) { formatter.keyword(':').prepend(Formatting.noSpace()); formatter.keyword(':').append(Formatting.oneSpace()); - } else if (ast.isModel(node)) { - const model = node as ast.Model; - const nodes = formatter.nodes(...model.declarations); - nodes.prepend(Formatting.noIndent()); - } - - // Should always run this no matter what formatting style it uses - if (ast.isAbstractDeclaration(node)) { + } else if (ast.isAbstractDeclaration(node)) { const bracesOpen = formatter.keyword('{'); const bracesClose = formatter.keyword('}'); // allow extra blank lines between declarations formatter.interior(bracesOpen, bracesClose).prepend(Formatting.indent({ allowMore: true })); bracesOpen.prepend(Formatting.oneSpace()); bracesClose.prepend(Formatting.newLine()); + } else if (ast.isModel(node)) { + const model = node as ast.Model; + const nodes = formatter.nodes(...model.declarations); + nodes.prepend(Formatting.noIndent()); } }