prisma-json-types-generator
Advanced tools
Comparing version
{ | ||
"name": "prisma-json-types-generator", | ||
"version": "2.5.0", | ||
"version": "3.0.0-beta.1", | ||
"description": "Changes JsonValues to your custom typescript type", | ||
@@ -17,19 +17,29 @@ "keywords": [ | ||
"author": "Arthur Fiorette <npm@arthur.place>", | ||
"main": "dist/generator.js", | ||
"bin": "./dist/bin.js", | ||
"main": "./index.js", | ||
"bin": "./index.js", | ||
"scripts": { | ||
"build": "tsc", | ||
"dev": "tsc -w", | ||
"format": "prettier --write .", | ||
"start": "node dist/bin.js", | ||
"test": "sh ./scripts/test.sh" | ||
}, | ||
"dependencies": { | ||
"@prisma/generator-helper": "4.16.0", | ||
"tslib": "2.5.3" | ||
"@prisma/generator-helper": "4.16.2", | ||
"tslib": "2.6.0" | ||
}, | ||
"devDependencies": { | ||
"@arthurfiorette/prettier-config": "1.0.9", | ||
"@prisma/client": "4.16.0", | ||
"@types/node": "20.3.1", | ||
"@prisma/client": "5.1.1", | ||
"@types/node": "20.3.3", | ||
"@types/prettier": "2.7.3", | ||
"prettier": "2.8.8", | ||
"prisma": "4.16.0", | ||
"prisma": "^5.1.1", | ||
"source-map-support": "^0.5.21", | ||
"tsd": "^0.28.1", | ||
"typescript": "5.1.3" | ||
}, | ||
"peerDependencies": { | ||
"prisma": "^4.16.0" | ||
"prisma": "^5.1", | ||
"typescript": "^5.1" | ||
}, | ||
@@ -39,10 +49,3 @@ "packageManager": "pnpm@8.4.0", | ||
"node": ">=14.0" | ||
}, | ||
"scripts": { | ||
"build": "tsc", | ||
"dev": "tsc -w", | ||
"format": "prettier --write .", | ||
"start": "node dist/bin.js", | ||
"test": "prisma generate && tsc --noEmit -p tsconfig.test.json" | ||
} | ||
} | ||
} |
@@ -35,8 +35,2 @@ <p align="center"> | ||
provider = "prisma-json-types-generator" | ||
// namespace = "PrismaJson" | ||
// clientOutput = "<finds it automatically>" | ||
// (./ -> relative to schema, or an importable path to require() it) | ||
// useType = "MyType" | ||
// In case you need to use a type, export it inside the namespace and we will add a index signature to it | ||
// (e.g. export namespace PrismaJson { export type MyType = {a: 1, b: 2} }; will generate namespace.MyType["TYPE HERE"]) | ||
} | ||
@@ -56,2 +50,5 @@ | ||
complex Json | ||
/// ![number | string] | ||
literal Json | ||
} | ||
@@ -70,2 +67,3 @@ ``` | ||
type MyType = boolean; | ||
// or you can use classes, interfaces, object types, etc. | ||
@@ -93,5 +91,55 @@ type ComplexType = { | ||
// example.complex is now a { foo: string; bar: number } | ||
// example.literal is now a string | number | ||
} | ||
``` | ||
### Configuration | ||
````ts | ||
export interface PrismaJsonTypesGeneratorConfig { | ||
/** | ||
* The namespace to generate the types in. | ||
* | ||
* @default 'PrismaJson' | ||
*/ | ||
namespace: string; | ||
/** | ||
* The name of the client output type. By default it will try to find it automatically | ||
* | ||
* (./ -> relative to schema, or an importable path to require() it) | ||
* | ||
* @default undefined | ||
*/ | ||
clientOutput?: string; | ||
/** | ||
* In case you need to use a type, export it inside the namespace and we will add a | ||
* index signature to it | ||
* | ||
* @example | ||
* | ||
* ```ts | ||
* export namespace PrismaJson { | ||
* export type GlobalType = { | ||
* fieldA: string; | ||
* fieldB: MyType; | ||
* }; | ||
* } | ||
* ``` | ||
* | ||
* @default undefined | ||
*/ | ||
useType?: string; | ||
/** | ||
* If we should allow untyped JSON fields to be any, otherwise we change them to | ||
* unknown. | ||
* | ||
* @default false | ||
*/ | ||
allowAny?: boolean; | ||
} | ||
```` | ||
### How it works | ||
@@ -98,0 +146,0 @@ |
@@ -5,2 +5,3 @@ import { generatorHandler } from '@prisma/generator-helper'; | ||
// Defines the entry point of the generator. | ||
generatorHandler({ | ||
@@ -7,0 +8,0 @@ onManifest, |
import ts from 'typescript'; | ||
import type { Declaration } from '../file/reader'; | ||
import type { ModelWithRegex } from '../helpers/dmmf'; | ||
import { replaceObject } from '../helpers/replace-object'; | ||
import { PrismaJsonTypesGeneratorConfig } from '../util/config'; | ||
import type { DeclarationWriter } from '../util/declaration-writer'; | ||
import { PrismaJsonTypesGeneratorError } from '../util/error'; | ||
import { replaceObject } from './replace-object'; | ||
export async function handleModelPayload( | ||
/** Replacer responsible for the main <Model>Payload type. */ | ||
export function handleModelPayload( | ||
typeAlias: ts.TypeAliasDeclaration, | ||
replacer: Declaration['replacer'], | ||
writer: DeclarationWriter, | ||
model: ModelWithRegex, | ||
nsName: string, | ||
useType?: string | ||
config: PrismaJsonTypesGeneratorConfig | ||
) { | ||
@@ -16,11 +18,14 @@ const type = typeAlias.type as ts.TypeLiteralNode; | ||
if (type.kind !== ts.SyntaxKind.TypeLiteral) { | ||
throw new Error( | ||
`prisma-json-types-generator: Provided model payload is not a type literal: ${type.getText()}` | ||
throw new PrismaJsonTypesGeneratorError( | ||
'Provided model payload is not a type literal', | ||
{ type: type.getText() } | ||
); | ||
} | ||
const scalarsField = type.members.find((m) => m.name?.getText() === 'scalars'); | ||
const scalarsField: any = type.members.find((m) => m.name?.getText() === 'scalars'); | ||
// Besides `scalars` field, the `objects` field also exists, but we don't need to handle it | ||
// because it just contains references to other <model>Payloads that we already change separately | ||
// Currently, there are 4 possible fields in the <model>Payload type: | ||
// - `scalars` field, which is what we mainly change | ||
// - `objects` are just references to other fields in which we change separately | ||
// - `name` and `composites` we do not have to change | ||
if (!scalarsField) { | ||
@@ -30,12 +35,15 @@ return; | ||
const object = ((scalarsField as ts.PropertySignature)?.type as ts.TypeReferenceNode) | ||
?.typeArguments?.[0] as ts.TypeLiteralNode; | ||
// Gets the inner object type we should change. | ||
// scalars format is: $Extensions.GetResult<OBJECT, ExtArgs["result"]["user"]> | ||
// this is the OBJECT part | ||
const object = scalarsField?.type?.typeArguments?.[0] as ts.TypeLiteralNode; | ||
if (!object) { | ||
throw new Error( | ||
`prisma-json-types-generator: Payload scalars could not be resolved: ${type.getText()}` | ||
); | ||
throw new PrismaJsonTypesGeneratorError('Payload scalars could not be resolved', { | ||
type: type.getText() | ||
}); | ||
} | ||
replaceObject(model, object, nsName, replacer, typeAlias.name.getText(), useType); | ||
// Replaces this object | ||
return replaceObject(object, writer, model, config); | ||
} |
import ts from 'typescript'; | ||
import type { Declaration } from '../file/reader'; | ||
import type { ModelWithRegex } from '../helpers/dmmf'; | ||
import { replaceSignature } from '../helpers/handle-signature'; | ||
import { JSON_REGEX } from '../helpers/regex'; | ||
import { PrismaJsonTypesGeneratorConfig } from '../util/config'; | ||
import { PRISMA_NAMESPACE_NAME } from '../util/constants'; | ||
import { DeclarationWriter } from '../util/declaration-writer'; | ||
import { PrismaJsonTypesGeneratorError } from '../util/error'; | ||
import { handleStatement } from './statement'; | ||
export async function handleModule( | ||
module: ts.ModuleDeclaration, | ||
replacer: Declaration['replacer'], | ||
/** Handles the prisma namespace module. */ | ||
export function handlePrismaModule( | ||
child: ts.ModuleDeclaration, | ||
writer: DeclarationWriter, | ||
models: ModelWithRegex[], | ||
nsName: string, | ||
useType?: string | ||
config: PrismaJsonTypesGeneratorConfig | ||
) { | ||
const namespace = module | ||
const name = child | ||
.getChildren() | ||
.find((n): n is ts.ModuleBlock => n.kind === ts.SyntaxKind.ModuleBlock); | ||
.find((n): n is ts.Identifier => n.kind === ts.SyntaxKind.Identifier); | ||
if (!namespace) { | ||
throw new Error('Prisma namespace could not be found'); | ||
// Not a prisma namespace | ||
if (!name || name.text !== PRISMA_NAMESPACE_NAME) { | ||
return; | ||
} | ||
for (const statement of namespace.statements) { | ||
const typeAlias = statement as ts.TypeAliasDeclaration; | ||
const content = child | ||
.getChildren() | ||
.find((n): n is ts.ModuleBlock => n.kind === ts.SyntaxKind.ModuleBlock); | ||
// Filters any statement that isn't a export type declaration | ||
if ( | ||
statement.kind !== ts.SyntaxKind.TypeAliasDeclaration || | ||
typeAlias.type.kind !== ts.SyntaxKind.TypeLiteral | ||
) { | ||
continue; | ||
} | ||
if (!content || !content.statements.length) { | ||
throw new PrismaJsonTypesGeneratorError( | ||
'Prisma namespace content could not be found' | ||
); | ||
} | ||
const typeAliasName = typeAlias.name.getText(); | ||
const typeAliasType = typeAlias.type as ts.TypeLiteralNode; | ||
// May includes the model name but is not the actual model, like | ||
// UserCreateWithoutPostsInput for Post model. that's why we need | ||
// to check if the model name is in the regex | ||
const model = models.find((m) => m.regexps.some((r) => r.test(typeAliasName))); | ||
if (!model) { | ||
continue; | ||
} | ||
const fields = model.fields.filter((f) => f.documentation?.match(JSON_REGEX)); | ||
for (const member of typeAliasType.members) { | ||
if (member.kind !== ts.SyntaxKind.PropertySignature) { | ||
continue; | ||
// Loops through all statements in the prisma namespace | ||
for (const statement of content.statements) { | ||
try { | ||
handleStatement(statement, writer, models, config); | ||
} catch (error) { | ||
// This allows some types to be generated even if others may fail | ||
// which is good for incremental development/testing | ||
if (error instanceof PrismaJsonTypesGeneratorError) { | ||
return PrismaJsonTypesGeneratorError.handler(error); | ||
} | ||
const signature = member as ts.PropertySignature; | ||
const fieldName = member.name?.getText(); | ||
const field = fields.find((f) => f.name === fieldName); | ||
if (!field || !fieldName) { | ||
continue; | ||
} | ||
if (!signature.type) { | ||
throw new Error( | ||
`prisma-json-types-generator: No type found for field ${fieldName} at model ${typeAliasName}` | ||
); | ||
} | ||
const typename = field.documentation?.match(JSON_REGEX)?.[1]; | ||
if (!typename) { | ||
throw new Error( | ||
`prisma-json-types-generator: No typename found for field ${fieldName} at model ${typeAliasName}` | ||
); | ||
} | ||
replaceSignature( | ||
signature.type, | ||
typename, | ||
nsName, | ||
replacer, | ||
fieldName, | ||
model.name, | ||
typeAliasName, | ||
useType | ||
); | ||
// Stops this generator is error thrown is not manually added by our code. | ||
throw error; | ||
} | ||
} | ||
} |
import type { DMMF } from '@prisma/generator-helper'; | ||
import { JSON_REGEX, regexForPrismaType } from './regex'; | ||
import { createRegexForType } from './regex'; | ||
export type ModelWithRegex = DMMF.Model & { | ||
/** A Prisma DMMF model with the regexes for each field. */ | ||
export interface ModelWithRegex extends DMMF.Model { | ||
regexps: RegExp[]; | ||
}; | ||
} | ||
export function parseDmmf(dmmf: DMMF.Document): ModelWithRegex[] { | ||
/** | ||
* Parses the DMMF document and returns a list of models that have at least one field with | ||
* typed json and the regexes for each field type. | ||
*/ | ||
export function extractPrismaModels(dmmf: DMMF.Document): ModelWithRegex[] { | ||
return ( | ||
dmmf.datamodel.models | ||
// All models that have at least one field with typed json | ||
.filter((m) => m.fields.some((f) => f.documentation?.match(JSON_REGEX))) | ||
.map((m) => ({ | ||
...m, | ||
// Loads all names and subnames regexes for the model | ||
regexps: regexForPrismaType(m.name) | ||
})) | ||
// Define the regexes for each model | ||
.map( | ||
(model): ModelWithRegex => ({ | ||
...model, | ||
regexps: createRegexForType(model.name) | ||
}) | ||
) | ||
); | ||
} |
@@ -1,2 +0,8 @@ | ||
export const JSON_REGEX = /^\s*\[(.*?)\]/m; | ||
/** | ||
* A regex to match the JSON output of a field's comment type. | ||
* | ||
* @example `[TYPE] comment...` | ||
*/ | ||
export const JSON_REGEX = /^\s*!?\[(.*?)\]/m; | ||
export const LITERAL_REGEX = /^\s*!?/m; | ||
@@ -7,3 +13,3 @@ /** | ||
*/ | ||
export function regexForPrismaType(name: string) { | ||
export function createRegexForType(name: string) { | ||
return [ | ||
@@ -37,4 +43,4 @@ new RegExp(`^${name}CountAggregate$`, 'm'), | ||
/** If the provided type is a update one variant */ | ||
export function isUpdateOne(type: string) { | ||
return type.match(/UpdateInput$/m) || type.match(/UpdateWithout(?:\\w+?)Input$/m); | ||
export function isUpdateOneType(type: string) { | ||
return type.match(/UpdateInput$/m) || type.match(/UpdateWithout(?:\w+?)Input$/m); | ||
} |
import type { GeneratorOptions } from '@prisma/generator-helper'; | ||
import ts from 'typescript'; | ||
import { readPrismaDeclarations } from './file/reader'; | ||
import { handleModule } from './handler/module'; | ||
import { handleTypeAlias } from './handler/type-alias'; | ||
import { parseDmmf } from './helpers/dmmf'; | ||
import { handlePrismaModule } from './handler/module'; | ||
import { extractPrismaModels } from './helpers/dmmf'; | ||
import { parseConfig } from './util/config'; | ||
import { DeclarationWriter } from './util/declaration-writer'; | ||
import { findPrismaClientGenerator } from './util/prisma-generator'; | ||
import { buildTypesFilePath } from './util/source-path'; | ||
/** Runs the generator with the given options. */ | ||
export async function onGenerate(options: GeneratorOptions) { | ||
const nsName = options.generator.config.namespace || 'PrismaJson'; | ||
const prismaClient = findPrismaClientGenerator(options.otherGenerators); | ||
const prismaClientOptions = options.otherGenerators.find((g) => g.name === 'client'); | ||
const config = parseConfig(options.generator.config); | ||
if (!prismaClientOptions) { | ||
throw new Error( | ||
'prisma-json-types-generator: Could not find client generator options, are you using prisma-client-js before prisma-json-types-generator?' | ||
); | ||
} | ||
if (!prismaClientOptions.output?.value) { | ||
throw new Error( | ||
'prisma-json-types-generator: prisma client output not found: ' + | ||
JSON.stringify(prismaClientOptions, null, 2) | ||
); | ||
} | ||
const { content, replacer, sourcePath, update } = await readPrismaDeclarations( | ||
nsName, | ||
prismaClientOptions.output.value, | ||
options.generator.config.clientOutput, | ||
const clientOutput = buildTypesFilePath( | ||
prismaClient.output.value, | ||
config.clientOutput, | ||
options.schemaPath | ||
); | ||
const writer = new DeclarationWriter(clientOutput, config); | ||
// Reads the prisma declaration file content. | ||
await writer.load(); | ||
const tsSource = ts.createSourceFile( | ||
sourcePath, | ||
content, | ||
writer.filepath, | ||
writer.content, | ||
ts.ScriptTarget.ESNext, | ||
@@ -41,37 +35,16 @@ true, | ||
const models = parseDmmf(options.dmmf); | ||
const prismaModels = extractPrismaModels(options.dmmf); | ||
const promises: Promise<void>[] = []; | ||
// Handles the prisma namespace. | ||
tsSource.forEachChild((child) => { | ||
switch (child.kind) { | ||
case ts.SyntaxKind.TypeAliasDeclaration: | ||
promises.push( | ||
handleTypeAlias( | ||
child as ts.TypeAliasDeclaration, | ||
replacer, | ||
models, | ||
nsName, | ||
options.generator.config.useType | ||
) | ||
); | ||
break; | ||
case ts.SyntaxKind.ModuleDeclaration: | ||
promises.push( | ||
handleModule( | ||
child as ts.ModuleDeclaration, | ||
replacer, | ||
models, | ||
nsName, | ||
options.generator.config.useType | ||
) | ||
); | ||
break; | ||
try { | ||
if (child.kind === ts.SyntaxKind.ModuleDeclaration) { | ||
handlePrismaModule(child as ts.ModuleDeclaration, writer, prismaModels, config); | ||
} | ||
} catch (error) { | ||
console.log(error); | ||
} | ||
}); | ||
await Promise.all(promises); | ||
await update(); | ||
await writer.save(); | ||
} |
@@ -5,5 +5,8 @@ import type { GeneratorManifest } from '@prisma/generator-helper'; | ||
/** Generates simple metadata for this generator. */ | ||
export function onManifest(): GeneratorManifest { | ||
return { | ||
version, | ||
// TODO: We should change this to the real output of the generator in some way. But we cannot get its real output here | ||
// because we need to await the prisma client to be generated first. | ||
defaultOutput: './', | ||
@@ -10,0 +13,0 @@ prettyName: 'Prisma Json Types Generator', |
@@ -92,3 +92,3 @@ /* prettier-ignore */ | ||
"noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ | ||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ | ||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ | ||
"noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ | ||
@@ -95,0 +95,0 @@ "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
182
35.82%2
-50%54614
-58.24%4
33.33%9
28.57%38
-54.76%1064
-47.51%1
Infinity%1
Infinity%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
Updated