Erster Docker-Stand

This commit is contained in:
Ali
2026-02-20 16:06:40 +09:00
commit f31e2e8ed3
8818 changed files with 1605323 additions and 0 deletions

View File

@@ -0,0 +1,608 @@
import * as schema from './getSchema';
import {
isOneOfSchemaObjects,
isSchemaField,
isSchemaObject,
} from './schemaUtils';
import { PrintOptions, printSchema } from './printSchema';
import * as finder from './finder';
/** Returns the function type Original with its return type changed to NewReturn. */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ReplaceReturnType<Original extends (...args: any) => any, NewReturn> = (
...a: Parameters<Original>
) => NewReturn;
/**
* Methods with return values that do not propagate the builder should not have
* their return value modified by the type replacement system below
* */
type ExtractKeys = 'getSchema' | 'getSubject' | 'getParent' | 'print';
/** These keys preserve the return value context that they were given */
type NeutralKeys =
| 'break'
| 'comment'
| 'attribute'
| 'enumerator'
| 'then'
| 'findByType'
| 'findAllByType';
/** Keys allowed when you call .datasource() or .generator() */
type DatasourceOrGeneratorKeys = 'assignment';
/** Keys allowed when you call .enum("name") */
type EnumKeys = 'enumerator';
/** Keys allowed when you call .field("name") */
type FieldKeys = 'attribute' | 'removeAttribute';
/** Keys allowed when you call .model("name") */
type BlockKeys = 'blockAttribute' | 'field' | 'removeField';
type PrismaSchemaFinderOptions = finder.ByTypeOptions & {
within?: finder.ByTypeSourceObject[];
};
/**
* Utility type for making the PrismaSchemaBuilder below readable:
* Removes methods from the builder that are prohibited based on the context
* the builder is in. For example, you can add fields to a model, but you can't
* add fields to an enum or a datasource.
*/
type PrismaSchemaSubset<
Universe extends keyof ConcretePrismaSchemaBuilder,
Method
> = ReplaceReturnType<
ConcretePrismaSchemaBuilder[Universe],
PrismaSchemaBuilder<Exclude<keyof ConcretePrismaSchemaBuilder, Method>>
>;
/**
* The brain of this whole operation: depending on the key of the method name
* we receive, filter the available list of method calls the user can make to
* prevent them from making invalid calls, such as builder.datasource().field()
* */
type PrismaSchemaBuilder<K extends keyof ConcretePrismaSchemaBuilder> = {
[U in K]: U extends ExtractKeys
? ConcretePrismaSchemaBuilder[U]
: U extends NeutralKeys
? ConcretePrismaSchemaBuilder[U] //ReplaceReturnType<ConcretePrismaSchemaBuilder[U], PrismaSchemaBuilder<K>>
: U extends 'datasource'
? PrismaSchemaSubset<U, 'datasource' | EnumKeys | FieldKeys | BlockKeys>
: U extends 'generator'
? PrismaSchemaSubset<U, EnumKeys | FieldKeys | BlockKeys>
: U extends 'model'
? PrismaSchemaSubset<U, DatasourceOrGeneratorKeys | EnumKeys | FieldKeys>
: U extends 'view'
? PrismaSchemaSubset<U, DatasourceOrGeneratorKeys | EnumKeys | FieldKeys>
: U extends 'type'
? PrismaSchemaSubset<U, DatasourceOrGeneratorKeys | EnumKeys | FieldKeys>
: U extends 'field'
? PrismaSchemaSubset<U, DatasourceOrGeneratorKeys | EnumKeys>
: U extends 'removeField'
? PrismaSchemaSubset<U, DatasourceOrGeneratorKeys | EnumKeys | FieldKeys>
: U extends 'enum'
? PrismaSchemaSubset<U, DatasourceOrGeneratorKeys | BlockKeys | FieldKeys>
: U extends 'removeAttribute'
? PrismaSchemaSubset<U, DatasourceOrGeneratorKeys | EnumKeys>
: PrismaSchemaSubset<
U,
DatasourceOrGeneratorKeys | EnumKeys | FieldKeys | BlockKeys | 'comment'
>;
};
type Arg =
| string
| {
name: string;
function?: Arg[];
};
type Parent = schema.Block | undefined;
type Subject = schema.Block | schema.Field | schema.Enumerator | undefined;
export class ConcretePrismaSchemaBuilder {
private schema: schema.Schema;
private _subject: Subject;
private _parent: Parent;
constructor(source = '') {
this.schema = schema.getSchema(source);
}
/** Prints the schema out as a source string */
print(options: PrintOptions = {}): string {
return printSchema(this.schema, options);
}
/** Returns the underlying schema object for more advanced use cases. */
getSchema(): schema.Schema {
return this.schema;
}
/** Mutation Methods */
/** Adds or updates a generator block based on the name. */
generator(name: string, provider = 'prisma-client-js'): this {
const generator: schema.Generator =
this.schema.list.reduce<schema.Generator>(
(memo, block) =>
block.type === 'generator' && block.name === name ? block : memo,
{
type: 'generator',
name,
assignments: [
{ type: 'assignment', key: 'provider', value: `"${provider}"` },
],
}
);
if (!this.schema.list.includes(generator)) this.schema.list.push(generator);
this._subject = generator;
return this;
}
/** Removes something from the schema with the given name. */
drop(name: string): this {
const index = this.schema.list.findIndex(
(block) => 'name' in block && block.name === name
);
if (index !== -1) this.schema.list.splice(index, 1);
return this;
}
/** Sets the datasource for the schema. */
datasource(provider: string, url: string | { env: string }): this {
const datasource: schema.Datasource = {
type: 'datasource',
name: 'db',
assignments: [
{
type: 'assignment',
key: 'url',
value:
typeof url === 'string'
? `"${url}"`
: { type: 'function', name: 'env', params: [`"${url.env}"`] },
},
{ type: 'assignment', key: 'provider', value: `"${provider}"` },
],
};
const existingIndex = this.schema.list.findIndex(
(block) => block.type === 'datasource'
);
this.schema.list.splice(
existingIndex,
existingIndex !== -1 ? 1 : 0,
datasource
);
this._subject = datasource;
return this;
}
/** Adds or updates a model based on the name. Can be chained with .field() or .blockAttribute() to add to it. */
model(name: string): this {
const model = this.schema.list.reduce<schema.Model>(
(memo, block) =>
block.type === 'model' && block.name === name ? block : memo,
{ type: 'model', name, properties: [] }
);
if (!this.schema.list.includes(model)) this.schema.list.push(model);
this._subject = model;
return this;
}
/** Adds or updates a view based on the name. Can be chained with .field() or .blockAttribute() to add to it. */
view(name: string): this {
const view = this.schema.list.reduce<schema.View>(
(memo, block) =>
block.type === 'view' && block.name === name ? block : memo,
{ type: 'view', name, properties: [] }
);
if (!this.schema.list.includes(view)) this.schema.list.push(view);
this._subject = view;
return this;
}
/** Adds or updates a type based on the name. Can be chained with .field() or .blockAttribute() to add to it. */
type(name: string): this {
const type = this.schema.list.reduce<schema.Type>(
(memo, block) =>
block.type === 'type' && block.name === name ? block : memo,
{ type: 'type', name, properties: [] }
);
if (!this.schema.list.includes(type)) this.schema.list.push(type);
this._subject = type;
return this;
}
/** Adds or updates an enum based on the name. Can be chained with .enumerator() to add a value to it. */
enum(name: string, enumeratorNames: string[] = []): this {
const e = this.schema.list.reduce<schema.Enum>(
(memo, block) =>
block.type === 'enum' && block.name === name ? block : memo,
{
type: 'enum',
name,
enumerators: enumeratorNames.map((name) => ({
type: 'enumerator',
name,
})),
} satisfies schema.Enum
);
if (!this.schema.list.includes(e)) this.schema.list.push(e);
this._subject = e;
return this;
}
/** Add an enum value to the current enum. */
enumerator(value: string): this {
const subject = this.getSubject<schema.Enum>();
if (!subject || !('type' in subject) || subject.type !== 'enum') {
throw new Error('Subject must be a prisma enum!');
}
const enumerator = {
type: 'enumerator',
name: value,
} satisfies schema.Enumerator;
subject.enumerators.push(enumerator);
this._parent = this._subject as Exclude<
Subject,
{ type: 'field' | 'enumerator' }
>;
this._subject = enumerator;
return this;
}
/**
* Returns the current subject, such as a model, field, or enum.
* @example
* builder.getModel('User').field('firstName').getSubject() // the firstName field
* */
private getSubject<S extends Subject>(): S {
return this._subject as S;
}
/** Returns the parent of the current subject when in a nested context. The parent of a field is its model or view. */
private getParent<S extends Parent = schema.Object>(): S {
return this._parent as S;
}
/**
* Adds a block-level attribute to the current model.
* @example
* builder.model('Project')
* .blockAttribute("map", "projects")
* .blockAttribute("unique", ["firstName", "lastName"]) // @@unique([firstName, lastName])
* */
blockAttribute(
name: string,
args?: string | string[] | Record<string, schema.Value>
): this {
let subject = this.getSubject<schema.Object | schema.Enum>();
if (subject.type !== 'enum' && !isSchemaObject(subject)) {
const parent = this.getParent<schema.Object>();
if (!isOneOfSchemaObjects(parent, ['model', 'view', 'type', 'enum']))
throw new Error('Subject must be a prisma model, view, or type!');
subject = this._subject = parent;
}
const attributeArgs = ((): schema.AttributeArgument[] => {
if (!args) return [] as schema.AttributeArgument[];
if (typeof args === 'string')
return [{ type: 'attributeArgument', value: `"${args}"` }];
if (Array.isArray(args))
return [{ type: 'attributeArgument', value: { type: 'array', args } }];
return Object.entries(args).map(([key, value]) => ({
type: 'attributeArgument',
value: { type: 'keyValue', key, value },
}));
})();
const property: schema.BlockAttribute = {
type: 'attribute',
kind: 'object',
name,
args: attributeArgs,
};
if (subject.type === 'enum') {
subject.enumerators.push(property);
} else {
subject.properties.push(property);
}
return this;
}
/** Adds an attribute to the current field. */
attribute<T extends schema.Field>(
name: string,
args?: Arg[] | Record<string, string[]>
): this {
const parent = this.getParent();
const subject = this.getSubject<T>();
if (!isOneOfSchemaObjects(parent, ['model', 'view', 'type', 'enum'])) {
throw new Error('Parent must be a prisma model or view!');
}
if (!isSchemaField(subject)) {
throw new Error('Subject must be a prisma field or enumerator!');
}
if (!subject.attributes) subject.attributes = [];
const attribute = subject.attributes.reduce<schema.Attribute>(
(memo, attr) =>
attr.type === 'attribute' &&
`${attr.group ? `${attr.group}.` : ''}${attr.name}` === name
? attr
: memo,
{
type: 'attribute',
kind: 'field',
name,
}
);
if (Array.isArray(args)) {
const mapArg = (arg: Arg): schema.Value | schema.Func => {
return typeof arg === 'string'
? arg
: {
type: 'function',
name: arg.name,
params: arg.function?.map(mapArg) ?? [],
};
};
if (args.length > 0)
attribute.args = args.map((arg) => ({
type: 'attributeArgument',
value: mapArg(arg),
}));
} else if (typeof args === 'object') {
attribute.args = Object.entries(args).map(([key, value]) => ({
type: 'attributeArgument',
value: { type: 'keyValue', key, value: { type: 'array', args: value } },
}));
}
if (!subject.attributes.includes(attribute))
subject.attributes.push(attribute);
return this;
}
/** Remove an attribute from the current field */
removeAttribute<T extends schema.Field>(name: string): this {
const parent = this.getParent();
const subject = this.getSubject<T>();
if (!isSchemaObject(parent)) {
throw new Error('Parent must be a prisma model or view!');
}
if (!isSchemaField(subject)) {
throw new Error('Subject must be a prisma field!');
}
if (!subject.attributes) subject.attributes = [];
subject.attributes = subject.attributes.filter(
(attr) => !(attr.type === 'attribute' && attr.name === name)
);
return this;
}
/** Add an assignment to a generator or datasource */
assignment<T extends schema.Generator | schema.Datasource>(
key: string,
value: string
): this {
const subject = this.getSubject<T>();
if (
!subject ||
!('type' in subject) ||
!['generator', 'datasource'].includes(subject.type)
)
throw new Error('Subject must be a prisma generator or datasource!');
function tap<T>(subject: T, callback: (s: T) => void) {
callback(subject);
return subject;
}
const assignment = subject.assignments.reduce<schema.Assignment>(
(memo, assignment) =>
assignment.type === 'assignment' && assignment.key === key
? tap(assignment, (a) => {
a.value = `"${value}"`;
})
: memo,
{
type: 'assignment',
key,
value: `"${value}"`,
}
);
if (!subject.assignments.includes(assignment))
subject.assignments.push(assignment);
return this;
}
/** Finder Methods */
/**
* Queries the block list for the given block type. Returns `null` if none
* match. Throws an error if more than one match is found.
* */
findByType<const Match extends finder.ByTypeMatch>(
typeToMatch: Match,
{ within = this.schema.list, ...options }: PrismaSchemaFinderOptions
): finder.FindByBlock<Match> | null {
return finder.findByType(within, typeToMatch, options);
}
/**
* Queries the block list for the given block type. Returns an array of all
* matching objects, and an empty array (`[]`) if none match.
* */
findAllByType<const Match extends finder.ByTypeMatch>(
typeToMatch: Match,
{ within = this.schema.list, ...options }: PrismaSchemaFinderOptions
): Array<finder.FindByBlock<Match> | null> {
return finder.findAllByType(within, typeToMatch, options);
}
/** Internal Utilities */
private blockInsert(statement: schema.Break | schema.Comment): this {
let subject = this.getSubject<schema.Block>();
const allowed = [
'datasource',
'enum',
'generator',
'model',
'view',
'type',
];
if (!subject || !('type' in subject) || !allowed.includes(subject.type)) {
const parent = this.getParent<schema.Block>();
if (!parent || !('type' in parent) || !allowed.includes(parent.type)) {
throw new Error('Subject must be a prisma block!');
}
subject = this._subject = parent;
}
switch (subject.type) {
case 'datasource': {
subject.assignments.push(statement);
break;
}
case 'enum': {
subject.enumerators.push(statement);
break;
}
case 'generator': {
subject.assignments.push(statement);
break;
}
case 'model': {
subject.properties.push(statement);
break;
}
}
return this;
}
/** Add a line break */
break(): this {
const lineBreak: schema.Break = { type: 'break' };
return this.blockInsert(lineBreak);
}
/**
* Add a comment. Regular comments start with // and do not appear in the
* prisma AST. Node comments start with /// and will appear in the AST,
* affixed to the node that follows the comment.
* */
comment(text: string, node = false): this {
const comment: schema.Comment = {
type: 'comment',
text: `//${node ? '/' : ''} ${text}`,
};
return this.blockInsert(comment);
}
/**
* Add a comment to the schema. Regular comments start with // and do not appear in the
* prisma AST. Node comments start with /// and will appear in the AST,
* affixed to the node that follows the comment.
* */
schemaComment(text: string, node = false): this {
const comment: schema.Comment = {
type: 'comment',
text: `//${node ? '/' : ''} ${text}`,
};
this.schema.list.push(comment);
return this;
}
/**
* Adds or updates a field in the current model. The field can be customized
* further with one or more .attribute() calls.
* */
field(name: string, fieldType: string | schema.Func = 'String'): this {
let subject = this.getSubject<schema.Object>();
if (!isSchemaObject(subject)) {
const parent = this.getParent<schema.Object>();
if (!isSchemaObject(parent))
throw new Error(
'Subject must be a prisma model or view or composite type!'
);
subject = this._subject = parent;
}
const field = subject.properties.reduce<schema.Field>(
(memo, block) =>
block.type === 'field' && block.name === name ? block : memo,
{
type: 'field',
name,
fieldType,
}
);
if (!subject.properties.includes(field)) subject.properties.push(field);
this._parent = subject;
this._subject = field;
return this;
}
/** Drop a field from the current model or view or composite type. */
removeField(name: string): this {
let subject = this.getSubject<schema.Object>();
if (!isSchemaObject(subject)) {
const parent = this.getParent<schema.Object>();
if (!isSchemaObject(parent))
throw new Error(
'Subject must be a prisma model or view or composite type!'
);
subject = this._subject = parent;
}
subject.properties = subject.properties.filter(
(field) => !(field.type === 'field' && field.name === name)
);
return this;
}
/**
* Returns the current subject, allowing for more advanced ways of
* manipulating the schema.
* */
then<R extends NonNullable<Subject>>(
callback: (subject: R) => unknown
): this {
callback(this._subject as R);
return this;
}
}
export function createPrismaSchemaBuilder(
source?: string
): PrismaSchemaBuilder<
Exclude<
keyof ConcretePrismaSchemaBuilder,
DatasourceOrGeneratorKeys | EnumKeys | FieldKeys | BlockKeys
>
> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return new ConcretePrismaSchemaBuilder(source) as any;
}

View File

@@ -0,0 +1,66 @@
import type * as schema from './getSchema';
export type ByTypeSourceObject =
| schema.Block
| schema.Enumerator
| schema.Field
| schema.Property
| schema.Attribute
| schema.Assignment;
export type ByTypeMatchObject = Exclude<
ByTypeSourceObject,
schema.Comment | schema.Break
>;
export type ByTypeMatch = ByTypeMatchObject['type'];
export type ByTypeOptions = { name?: string | RegExp };
export type FindByBlock<Match> = Extract<ByTypeMatchObject, { type: Match }>;
export const findByType = <const Match extends ByTypeMatch>(
list: ByTypeSourceObject[],
typeToMatch: Match,
options: ByTypeOptions = {}
): FindByBlock<Match> | null => {
const [match, unexpected] = list.filter(findBy(typeToMatch, options));
if (!match) return null;
if (unexpected)
throw new Error(`Found multiple blocks with [type=${typeToMatch}]`);
return match;
};
export const findAllByType = <const Match extends ByTypeMatch>(
list: ByTypeSourceObject[],
typeToMatch: Match,
options: ByTypeOptions = {}
): Array<FindByBlock<Match>> => {
return list.filter(findBy(typeToMatch, options));
};
type NameOf<Match extends ByTypeMatch> = Extract<
Match,
Match extends 'assignment' ? 'key' : 'name'
>;
const findBy =
<Match extends ByTypeMatch, MatchName extends NameOf<Match>>(
typeToMatch: Match,
{ name }: ByTypeOptions = {}
) =>
(block: ByTypeSourceObject): block is FindByBlock<Match> => {
if (name != null) {
const nameAttribute = (
typeToMatch === 'assignment' ? 'key' : 'name'
) as MatchName;
if (!(nameAttribute in block)) return false;
const nameMatches =
typeof name === 'string'
? block[nameAttribute] === name
: name.test(block[nameAttribute]);
if (!nameMatches) return false;
}
return block.type === typeToMatch;
};

View File

@@ -0,0 +1,27 @@
import type { IParserConfig } from 'chevrotain';
import {
lilconfigSync as configSync,
type LilconfigResult as ConfigResultRaw,
} from 'lilconfig';
export type PrismaAstParserConfig = Pick<IParserConfig, 'nodeLocationTracking'>;
export interface PrismaAstConfig {
parser: PrismaAstParserConfig;
}
type ConfigResult<T> = Omit<ConfigResultRaw, 'config'> & {
config: T;
};
const defaultConfig: PrismaAstConfig = {
parser: { nodeLocationTracking: 'none' },
};
let config: PrismaAstConfig;
export default function getConfig(): PrismaAstConfig {
if (config != null) return config;
const result: ConfigResult<PrismaAstConfig> | null =
configSync('prisma-ast').search();
return (config = Object.assign(defaultConfig, result?.config));
}

View File

@@ -0,0 +1,186 @@
import { PrismaLexer } from './lexer';
import { PrismaVisitor, defaultVisitor } from './visitor';
import type { CstNodeLocation } from 'chevrotain';
import { PrismaParser, defaultParser } from './parser';
/**
* Parses a string containing a prisma schema's source code and returns an
* object that represents the parsed data structure. You can make direct
* modifications to the objects and arrays nested within, and then produce
* a new prisma schema using printSchema().
*
* @example
* const schema = getSchema(source)
* // ... make changes to schema object ...
* const changedSource = printSchema(schema)
* */
export function getSchema(
source: string,
options?: {
parser: PrismaParser;
visitor: PrismaVisitor;
}
): Schema {
const lexingResult = PrismaLexer.tokenize(source);
const parser = options?.parser ?? defaultParser;
parser.input = lexingResult.tokens;
const cstNode = parser.schema();
if (parser.errors.length > 0) throw parser.errors[0];
const visitor = options?.visitor ?? defaultVisitor;
return visitor.visit(cstNode);
}
export interface Schema {
type: 'schema';
list: Block[];
}
export type Block =
| Model
| View
| Datasource
| Generator
| Enum
| Comment
| Break
| Type;
export interface Object {
type: 'model' | 'view' | 'type';
name: string;
properties: Array<Property | Comment | Break>;
}
export interface Model extends Object {
type: 'model';
location?: CstNodeLocation;
}
export interface View extends Object {
type: 'view';
location?: CstNodeLocation;
}
export interface Type extends Object {
type: 'type';
location?: CstNodeLocation;
}
export interface Datasource {
type: 'datasource';
name: string;
assignments: Array<Assignment | Comment | Break>;
location?: CstNodeLocation;
}
export interface Generator {
type: 'generator';
name: string;
assignments: Array<Assignment | Comment | Break>;
location?: CstNodeLocation;
}
export interface Enum {
type: 'enum';
name: string;
enumerators: Array<
Enumerator | Comment | Break | BlockAttribute | GroupedAttribute
>;
location?: CstNodeLocation;
}
export interface Comment {
type: 'comment';
text: string;
}
export interface Break {
type: 'break';
}
export type Property = GroupedBlockAttribute | BlockAttribute | Field;
export interface Assignment {
type: 'assignment';
key: string;
value: Value;
}
export interface Enumerator {
type: 'enumerator';
name: string;
value?: Value;
attributes?: Attribute[];
comment?: string;
}
export interface BlockAttribute {
type: 'attribute';
kind: 'object' | 'view' | 'type';
group?: string;
name: string;
args: AttributeArgument[];
location?: CstNodeLocation;
}
export type GroupedBlockAttribute = BlockAttribute & { group: string };
export interface Field {
type: 'field';
name: string;
fieldType: string | Func;
array?: boolean;
optional?: boolean;
attributes?: Attribute[];
comment?: string;
location?: CstNodeLocation;
}
export type Attr =
| Attribute
| GroupedAttribute
| BlockAttribute
| GroupedBlockAttribute;
export interface Attribute {
type: 'attribute';
kind: 'field';
group?: string;
name: string;
args?: AttributeArgument[];
location?: CstNodeLocation;
}
export type GroupedAttribute = Attribute & { group: string };
export interface AttributeArgument {
type: 'attributeArgument';
value: KeyValue | Value | Func;
}
export interface KeyValue {
type: 'keyValue';
key: string;
value: Value;
}
export interface Func {
type: 'function';
name: string;
params?: Value[];
}
export interface RelationArray {
type: 'array';
args: string[];
}
export type Value =
| string
| number
| boolean
| Func
| RelationArray
| Array<Value>;

View File

@@ -0,0 +1,8 @@
export * from './produceSchema';
export * from './getSchema';
export * from './printSchema';
export * from './PrismaSchemaBuilder';
export type { PrismaAstConfig } from './getConfig';
export type { CstNodeLocation } from 'chevrotain';
export { VisitorClassFactory } from './visitor';
export { PrismaParser } from './parser';

View File

@@ -0,0 +1,191 @@
import { createToken, Lexer, IMultiModeLexerDefinition } from 'chevrotain';
export const Identifier = createToken({
name: 'Identifier',
pattern: /[a-zA-Z][\w-]*/,
});
export const Datasource = createToken({
name: 'Datasource',
pattern: /datasource/,
push_mode: 'block',
});
export const Generator = createToken({
name: 'Generator',
pattern: /generator/,
push_mode: 'block',
});
export const Model = createToken({
name: 'Model',
pattern: /model/,
push_mode: 'block',
});
export const View = createToken({
name: 'View',
pattern: /view/,
push_mode: 'block',
});
export const Enum = createToken({
name: 'Enum',
pattern: /enum/,
push_mode: 'block',
});
export const Type = createToken({
name: 'Type',
pattern: /type/,
push_mode: 'block',
});
export const True = createToken({
name: 'True',
pattern: /true/,
longer_alt: Identifier,
});
export const False = createToken({
name: 'False',
pattern: /false/,
longer_alt: Identifier,
});
export const Null = createToken({
name: 'Null',
pattern: /null/,
longer_alt: Identifier,
});
export const Comment = createToken({
name: 'Comment',
pattern: Lexer.NA,
});
export const DocComment = createToken({
name: 'DocComment',
pattern: /\/\/\/[ \t]*(.*)/,
categories: [Comment],
});
export const LineComment = createToken({
name: 'LineComment',
pattern: /\/\/[ \t]*(.*)/,
categories: [Comment],
});
export const Attribute = createToken({
name: 'Attribute',
pattern: Lexer.NA,
});
export const BlockAttribute = createToken({
name: 'BlockAttribute',
pattern: /@@/,
label: "'@@'",
categories: [Attribute],
});
export const FieldAttribute = createToken({
name: 'FieldAttribute',
pattern: /@/,
label: "'@'",
categories: [Attribute],
});
export const Dot = createToken({
name: 'Dot',
pattern: /\./,
label: "'.'",
});
export const QuestionMark = createToken({
name: 'QuestionMark',
pattern: /\?/,
label: "'?'",
});
export const LCurly = createToken({
name: 'LCurly',
pattern: /{/,
label: "'{'",
});
export const RCurly = createToken({
name: 'RCurly',
pattern: /}/,
label: "'}'",
pop_mode: true,
});
export const LRound = createToken({
name: 'LRound',
pattern: /\(/,
label: "'('",
});
export const RRound = createToken({
name: 'RRound',
pattern: /\)/,
label: "')'",
});
export const LSquare = createToken({
name: 'LSquare',
pattern: /\[/,
label: "'['",
});
export const RSquare = createToken({
name: 'RSquare',
pattern: /\]/,
label: "']'",
});
export const Comma = createToken({
name: 'Comma',
pattern: /,/,
label: "','",
});
export const Colon = createToken({
name: 'Colon',
pattern: /:/,
label: "':'",
});
export const Equals = createToken({
name: 'Equals',
pattern: /=/,
label: "'='",
});
export const StringLiteral = createToken({
name: 'StringLiteral',
pattern: /"(:?[^\\"\n\r]|\\(:?[bfnrtv"\\/]|u[0-9a-fA-F]{4}))*"/,
});
export const NumberLiteral = createToken({
name: 'NumberLiteral',
pattern: /-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/,
});
export const WhiteSpace = createToken({
name: 'WhiteSpace',
pattern: /\s+/,
group: Lexer.SKIPPED,
});
export const LineBreak = createToken({
name: 'LineBreak',
pattern: /\n|\r\n/,
line_breaks: true,
label: 'LineBreak',
});
const naTokens = [Comment, DocComment, LineComment, LineBreak, WhiteSpace];
export const multiModeTokens: IMultiModeLexerDefinition = {
modes: {
global: [...naTokens, Datasource, Generator, Model, View, Enum, Type],
block: [
...naTokens,
Attribute,
BlockAttribute,
FieldAttribute,
Dot,
QuestionMark,
LCurly,
RCurly,
LSquare,
RSquare,
LRound,
RRound,
Comma,
Colon,
Equals,
True,
False,
Null,
StringLiteral,
NumberLiteral,
Identifier,
],
},
defaultMode: 'global',
};
export const PrismaLexer = new Lexer(multiModeTokens);

View File

@@ -0,0 +1,267 @@
import { CstParser } from 'chevrotain';
import getConfig, { PrismaAstParserConfig } from './getConfig';
import * as lexer from './lexer';
type ComponentType =
| 'datasource'
| 'generator'
| 'model'
| 'view'
| 'enum'
| 'type';
export class PrismaParser extends CstParser {
readonly config: PrismaAstParserConfig;
constructor(config: PrismaAstParserConfig) {
super(lexer.multiModeTokens, config);
this.performSelfAnalysis();
this.config = config;
}
private break = this.RULE('break', () => {
this.CONSUME1(lexer.LineBreak);
this.CONSUME2(lexer.LineBreak);
});
private keyedArg = this.RULE('keyedArg', () => {
this.CONSUME(lexer.Identifier, { LABEL: 'keyName' });
this.CONSUME(lexer.Colon);
this.SUBRULE(this.value);
});
private array = this.RULE('array', () => {
this.CONSUME(lexer.LSquare);
this.MANY_SEP({
SEP: lexer.Comma,
DEF: () => {
this.SUBRULE(this.value);
},
});
this.CONSUME(lexer.RSquare);
});
private func = this.RULE('func', () => {
this.CONSUME(lexer.Identifier, { LABEL: 'funcName' });
this.CONSUME(lexer.LRound);
this.MANY_SEP({
SEP: lexer.Comma,
DEF: () => {
this.OR([
{ ALT: () => this.SUBRULE(this.keyedArg) },
{ ALT: () => this.SUBRULE(this.value) },
]);
},
});
this.CONSUME(lexer.RRound);
});
private value = this.RULE('value', () => {
this.OR([
{ ALT: () => this.CONSUME(lexer.StringLiteral, { LABEL: 'value' }) },
{ ALT: () => this.CONSUME(lexer.NumberLiteral, { LABEL: 'value' }) },
{ ALT: () => this.SUBRULE(this.array, { LABEL: 'value' }) },
{ ALT: () => this.SUBRULE(this.func, { LABEL: 'value' }) },
{ ALT: () => this.CONSUME(lexer.True, { LABEL: 'value' }) },
{ ALT: () => this.CONSUME(lexer.False, { LABEL: 'value' }) },
{ ALT: () => this.CONSUME(lexer.Null, { LABEL: 'value' }) },
{ ALT: () => this.CONSUME(lexer.Identifier, { LABEL: 'value' }) },
]);
});
private property = this.RULE('property', () => {
this.CONSUME(lexer.Identifier, { LABEL: 'propertyName' });
this.CONSUME(lexer.Equals);
this.SUBRULE(this.value, { LABEL: 'propertyValue' });
});
private assignment = this.RULE('assignment', () => {
this.CONSUME(lexer.Identifier, { LABEL: 'assignmentName' });
this.CONSUME(lexer.Equals);
this.SUBRULE(this.value, { LABEL: 'assignmentValue' });
});
private field = this.RULE('field', () => {
this.CONSUME(lexer.Identifier, { LABEL: 'fieldName' });
this.SUBRULE(this.value, { LABEL: 'fieldType' });
this.OPTION1(() => {
this.OR([
{
ALT: () => {
this.CONSUME(lexer.LSquare, { LABEL: 'array' });
this.CONSUME(lexer.RSquare, { LABEL: 'array' });
},
},
{ ALT: () => this.CONSUME(lexer.QuestionMark, { LABEL: 'optional' }) },
]);
});
this.MANY(() => {
this.SUBRULE(this.fieldAttribute, { LABEL: 'attributeList' });
});
this.OPTION2(() => {
this.CONSUME(lexer.Comment, { LABEL: 'comment' });
});
});
private block = this.RULE(
'block',
(
options: {
componentType?: ComponentType;
} = {}
) => {
const { componentType } = options;
const isEnum = componentType === 'enum';
const isObject =
componentType === 'model' ||
componentType === 'view' ||
componentType === 'type';
this.CONSUME(lexer.LCurly);
this.CONSUME1(lexer.LineBreak);
this.MANY(() => {
this.OR([
{ ALT: () => this.SUBRULE(this.comment, { LABEL: 'list' }) },
{
GATE: () => isObject,
ALT: () => this.SUBRULE(this.property, { LABEL: 'list' }),
},
{ ALT: () => this.SUBRULE(this.blockAttribute, { LABEL: 'list' }) },
{
GATE: () => isObject,
ALT: () => this.SUBRULE(this.field, { LABEL: 'list' }),
},
{
GATE: () => isEnum,
ALT: () => this.SUBRULE(this.enum, { LABEL: 'list' }),
},
{
GATE: () => !isObject,
ALT: () => this.SUBRULE(this.assignment, { LABEL: 'list' }),
},
{ ALT: () => this.SUBRULE(this.break, { LABEL: 'list' }) },
{ ALT: () => this.CONSUME2(lexer.LineBreak) },
]);
});
this.CONSUME(lexer.RCurly);
}
);
private enum = this.RULE('enum', () => {
this.CONSUME(lexer.Identifier, { LABEL: 'enumName' });
this.MANY(() => {
this.SUBRULE(this.fieldAttribute, { LABEL: 'attributeList' });
});
this.OPTION(() => {
this.CONSUME(lexer.Comment, { LABEL: 'comment' });
});
});
private fieldAttribute = this.RULE('fieldAttribute', () => {
this.CONSUME(lexer.FieldAttribute, { LABEL: 'fieldAttribute' });
this.OR([
{
ALT: () => {
this.CONSUME1(lexer.Identifier, { LABEL: 'groupName' });
this.CONSUME(lexer.Dot);
this.CONSUME2(lexer.Identifier, { LABEL: 'attributeName' });
},
},
{
ALT: () => this.CONSUME(lexer.Identifier, { LABEL: 'attributeName' }),
},
]);
this.OPTION(() => {
this.CONSUME(lexer.LRound);
this.MANY_SEP({
SEP: lexer.Comma,
DEF: () => {
this.SUBRULE(this.attributeArg);
},
});
this.CONSUME(lexer.RRound);
});
});
private blockAttribute = this.RULE('blockAttribute', () => {
this.CONSUME(lexer.BlockAttribute, { LABEL: 'blockAttribute' }),
this.OR([
{
ALT: () => {
this.CONSUME1(lexer.Identifier, { LABEL: 'groupName' });
this.CONSUME(lexer.Dot);
this.CONSUME2(lexer.Identifier, { LABEL: 'attributeName' });
},
},
{
ALT: () => this.CONSUME(lexer.Identifier, { LABEL: 'attributeName' }),
},
]);
this.OPTION(() => {
this.CONSUME(lexer.LRound);
this.MANY_SEP({
SEP: lexer.Comma,
DEF: () => {
this.SUBRULE(this.attributeArg);
},
});
this.CONSUME(lexer.RRound);
});
});
private attributeArg = this.RULE('attributeArg', () => {
this.OR([
{
ALT: () => this.SUBRULE(this.keyedArg, { LABEL: 'value' }),
},
{
ALT: () => this.SUBRULE(this.value, { LABEL: 'value' }),
},
]);
});
private component = this.RULE('component', () => {
const type = this.OR1([
{ ALT: () => this.CONSUME(lexer.Datasource, { LABEL: 'type' }) },
{ ALT: () => this.CONSUME(lexer.Generator, { LABEL: 'type' }) },
{ ALT: () => this.CONSUME(lexer.Model, { LABEL: 'type' }) },
{ ALT: () => this.CONSUME(lexer.View, { LABEL: 'type' }) },
{ ALT: () => this.CONSUME(lexer.Enum, { LABEL: 'type' }) },
{ ALT: () => this.CONSUME(lexer.Type, { LABEL: 'type' }) },
]);
this.OR2([
{
ALT: () => {
this.CONSUME1(lexer.Identifier, { LABEL: 'groupName' });
this.CONSUME(lexer.Dot);
this.CONSUME2(lexer.Identifier, { LABEL: 'componentName' });
},
},
{
ALT: () => this.CONSUME(lexer.Identifier, { LABEL: 'componentName' }),
},
]);
this.SUBRULE(this.block, {
ARGS: [{ componentType: type.image as ComponentType }],
});
});
private comment = this.RULE('comment', () => {
this.CONSUME(lexer.Comment, { LABEL: 'text' });
});
public schema = this.RULE('schema', () => {
this.MANY(() => {
this.OR([
{ ALT: () => this.SUBRULE(this.comment, { LABEL: 'list' }) },
{ ALT: () => this.SUBRULE(this.component, { LABEL: 'list' }) },
{ ALT: () => this.SUBRULE(this.break, { LABEL: 'list' }) },
{ ALT: () => this.CONSUME(lexer.LineBreak) },
]);
});
});
}
export const defaultParser = new PrismaParser(getConfig().parser);

View File

@@ -0,0 +1,389 @@
import * as Types from './getSchema';
import { EOL } from 'os';
import { schemaSorter } from './schemaSorter';
type Block = 'generator' | 'datasource' | 'model' | 'view' | 'enum' | 'type';
export interface PrintOptions {
sort?: boolean;
locales?: string | string[];
sortOrder?: Block[];
}
/**
* Converts the given schema object into a string representing the prisma
* schema's source code. Optionally can take options to change the sort order
* of the schema parts.
* */
export function printSchema(
schema: Types.Schema,
options: PrintOptions = {}
): string {
const { sort = false, locales = undefined, sortOrder = undefined } = options;
let blocks = schema.list;
if (sort) {
// no point in preserving line breaks when re-sorting
blocks = schema.list = blocks.filter((block) => block.type !== 'break');
const sorter = schemaSorter(schema, locales, sortOrder);
blocks.sort(sorter);
}
return (
blocks
.map(printBlock)
.filter(Boolean)
.join(EOL)
.replace(/(\r?\n\s*){3,}/g, EOL + EOL) + EOL
);
}
function printBlock(block: Types.Block): string {
switch (block.type) {
case 'comment':
return printComment(block);
case 'datasource':
return printDatasource(block);
case 'enum':
return printEnum(block);
case 'generator':
return printGenerator(block);
case 'model':
case 'view':
case 'type':
return printObject(block);
case 'break':
return printBreak();
default:
throw new Error(`Unrecognized block type`);
}
}
function printComment(comment: Types.Comment) {
return comment.text;
}
function printBreak() {
return EOL;
}
function printDatasource(db: Types.Datasource) {
const children = computeAssignmentFormatting(db.assignments);
return `
datasource ${db.name} {
${children}
}`;
}
function printEnum(enumerator: Types.Enum) {
const list: Array<
| Types.Comment
| Types.Break
| Types.Enumerator
| Types.BlockAttribute
| Types.GroupedBlockAttribute
| Types.GroupedAttribute
> = enumerator.enumerators;
const children = list
.filter(Boolean)
.map(printEnumerator)
.join(`${EOL} `)
.replace(/(\r?\n\s*){3,}/g, `${EOL + EOL} `);
return `
enum ${enumerator.name} {
${children}
}`;
}
function printEnumerator(
enumerator:
| Types.Enumerator
| Types.Attribute
| Types.Comment
| Types.Break
| Types.BlockAttribute
| Types.GroupedBlockAttribute
| Types.GroupedAttribute
) {
switch (enumerator.type) {
case 'enumerator': {
const attrs = enumerator.attributes
? enumerator.attributes.map(printAttribute)
: [];
return [enumerator.name, ...attrs, enumerator.comment]
.filter(Boolean)
.join(' ');
}
case 'attribute':
return printAttribute(enumerator);
case 'comment':
return printComment(enumerator);
case 'break':
return printBreak();
default:
throw new Error(`Unexpected enumerator type`);
}
}
function printGenerator(generator: Types.Generator) {
const children = computeAssignmentFormatting(generator.assignments);
return `
generator ${generator.name} {
${children}
}`;
}
function printObject(object: Types.Object) {
const props = [...object.properties];
// If block attributes are declared in the middle of the block, move them to
// the bottom of the list.
let blockAttributeMoved = false;
props.sort((a, b) => {
if (
a.type === 'attribute' &&
a.kind === 'object' &&
(b.type !== 'attribute' ||
(b.type === 'attribute' && b.kind !== 'object'))
) {
blockAttributeMoved = true;
return 1;
}
if (
b.type === 'attribute' &&
b.kind === 'object' &&
(a.type !== 'attribute' ||
(a.type === 'attribute' && a.kind !== 'object'))
) {
blockAttributeMoved = true;
return -1;
}
return 0;
});
// Insert a break between the block attributes and the file if the block
// attributes are too close to the model's fields
const attrIndex = props.findIndex(
(item) => item.type === 'attribute' && item.kind === 'object'
);
const needsSpace = !['break', 'comment'].includes(props[attrIndex - 1]?.type);
if (blockAttributeMoved && needsSpace) {
props.splice(attrIndex, 0, { type: 'break' });
}
const children = computePropertyFormatting(props);
return `
${object.type} ${object.name} {
${children}
}`;
}
function printAssignment(
node: Types.Assignment | Types.Comment | Types.Break,
keyLength = 0
) {
switch (node.type) {
case 'comment':
return printComment(node);
case 'break':
return printBreak();
case 'assignment':
return `${node.key.padEnd(keyLength)} = ${printValue(node.value)}`;
default:
throw new Error(`Unexpected assignment type`);
}
}
function printProperty(
node: Types.Property | Types.Comment | Types.Break,
nameLength = 0,
typeLength = 0
) {
switch (node.type) {
case 'attribute':
return printAttribute(node);
case 'field':
return printField(node, nameLength, typeLength);
case 'comment':
return printComment(node);
case 'break':
return printBreak();
default:
throw new Error(`Unrecognized property type`);
}
}
function printAttribute(attribute: Types.Attribute | Types.BlockAttribute) {
const args =
attribute.args && attribute.args.length > 0
? `(${attribute.args.map(printAttributeArg).filter(Boolean).join(', ')})`
: '';
const name = [attribute.name];
if (attribute.group) name.unshift(attribute.group);
return `${attribute.kind === 'field' ? '@' : '@@'}${name.join('.')}${args}`;
}
function printAttributeArg(arg: Types.AttributeArgument) {
return printValue(arg.value);
}
function printField(field: Types.Field, nameLength = 0, typeLength = 0) {
const name = field.name.padEnd(nameLength);
const fieldType = printFieldType(field).padEnd(typeLength);
const attrs = field.attributes ? field.attributes.map(printAttribute) : [];
const comment = field.comment;
return (
[name, fieldType, ...attrs]
.filter(Boolean)
.join(' ')
// comments ignore indents
.trim() + (comment ? ` ${comment}` : '')
);
}
function printFieldType(field: Types.Field) {
const suffix = field.array ? '[]' : field.optional ? '?' : '';
if (typeof field.fieldType === 'object') {
switch (field.fieldType.type) {
case 'function': {
return `${printFunction(field.fieldType)}${suffix}`;
}
default:
throw new Error(`Unexpected field type`);
}
}
return `${field.fieldType}${suffix}`;
}
function printFunction(func: Types.Func) {
const params = func.params ? func.params.map(printValue) : '';
return `${func.name}(${params})`;
}
function printValue(value: Types.KeyValue | Types.Value): string {
switch (typeof value) {
case 'object': {
if ('type' in value) {
switch (value.type) {
case 'keyValue':
return `${value.key}: ${printValue(value.value)}`;
case 'function':
return printFunction(value);
case 'array':
return `[${
value.args != null ? value.args.map(printValue).join(', ') : ''
}]`;
default:
throw new Error(`Unexpected value type`);
}
}
throw new Error(`Unexpected object value`);
}
default:
return String(value);
}
}
function computeAssignmentFormatting(
list: Array<Types.Comment | Types.Break | Types.Assignment>
) {
let pos = 0;
const listBlocks = list.reduce<Array<typeof list>>(
(memo, current, index, arr) => {
if (current.type === 'break') return memo;
if (index > 0 && arr[index - 1].type === 'break') memo[++pos] = [];
memo[pos].push(current);
return memo;
},
[[]]
);
const keyLengths = listBlocks.map((lists) =>
lists.reduce(
(max, current) =>
Math.max(
max,
// perhaps someone more typescript-savy than I am can fix this
current.type === 'assignment' ? current.key.length : 0
),
0
)
);
return list
.map((item, index, arr) => {
if (index > 0 && item.type !== 'break' && arr[index - 1].type === 'break')
keyLengths.shift();
return printAssignment(item, keyLengths[0]);
})
.filter(Boolean)
.join(`${EOL} `)
.replace(/(\r?\n\s*){3,}/g, `${EOL + EOL} `);
}
function computePropertyFormatting(
list: Array<Types.Break | Types.Comment | Types.Property>
) {
let pos = 0;
const listBlocks = list.reduce<Array<typeof list>>(
(memo, current, index, arr) => {
if (current.type === 'break') return memo;
if (index > 0 && arr[index - 1].type === 'break') memo[++pos] = [];
memo[pos].push(current);
return memo;
},
[[]]
);
const nameLengths = listBlocks.map((lists) =>
lists.reduce(
(max, current) =>
Math.max(
max,
// perhaps someone more typescript-savy than I am can fix this
current.type === 'field' ? current.name.length : 0
),
0
)
);
const typeLengths = listBlocks.map((lists) =>
lists.reduce(
(max, current) =>
Math.max(
max,
// perhaps someone more typescript-savy than I am can fix this
current.type === 'field' ? printFieldType(current).length : 0
),
0
)
);
return list
.map((prop, index, arr) => {
if (
index > 0 &&
prop.type !== 'break' &&
arr[index - 1].type === 'break'
) {
nameLengths.shift();
typeLengths.shift();
}
return printProperty(prop, nameLengths[0], typeLengths[0]);
})
.filter(Boolean)
.join(`${EOL} `)
.replace(/(\r?\n\s*){3,}/g, `${EOL + EOL} `);
}

View File

@@ -0,0 +1,19 @@
import { PrintOptions } from './printSchema';
import { createPrismaSchemaBuilder } from './PrismaSchemaBuilder';
type Options = PrintOptions;
/**
* Receives a prisma schema in the form of a string containing source code, and
* a callback builder function. Use the builder to modify your schema as
* desired. Returns the schema as a string with the modifications applied.
* */
export function produceSchema(
source: string,
producer: (builder: ReturnType<typeof createPrismaSchemaBuilder>) => void,
options: Options = {}
): string {
const builder = createPrismaSchemaBuilder(source);
producer(builder);
return builder.print(options);
}

View File

@@ -0,0 +1,43 @@
import { Block, Schema } from './getSchema';
const unsorted = ['break', 'comment'];
const defaultSortOrder = [
'generator',
'datasource',
'model',
'view',
'enum',
'break',
'comment',
];
/** Sorts the schema parts, in the given order, and alphabetically for parts of the same type. */
export const schemaSorter =
(
schema: Schema,
locales?: string | string[],
sortOrder: string[] = defaultSortOrder
) =>
(a: Block, b: Block): number => {
// Preserve the position of comments and line breaks relative to their
// position in the file, since when a re-sort happens it wouldn't be
// clear whether a comment should affix to the object above or below it.
const aUnsorted = unsorted.indexOf(a.type) !== -1;
const bUnsorted = unsorted.indexOf(b.type) !== -1;
if (aUnsorted !== bUnsorted) {
return schema.list.indexOf(a) - schema.list.indexOf(b);
}
if (sortOrder !== defaultSortOrder)
sortOrder = sortOrder.concat(defaultSortOrder);
const typeIndex = sortOrder.indexOf(a.type) - sortOrder.indexOf(b.type);
if (typeIndex !== 0) return typeIndex;
// Resolve ties using the name of object's name.
if ('name' in a && 'name' in b)
return a.name.localeCompare(b.name, locales);
// If all else fails, leave objects in their original position.
return 0;
};

View File

@@ -0,0 +1,72 @@
import type { CstNode, IToken } from 'chevrotain';
import * as schema from './getSchema';
const schemaObjects = ['model', 'view', 'type'] as const;
export function isOneOfSchemaObjects<T extends string>(
obj: schema.Object,
schemas: readonly T[]
): obj is Extract<schema.Object, { type: T }> {
return obj != null && 'type' in obj && schemas.includes(obj.type as T);
}
/** Returns true if the value is an Object, such as a model or view or composite type. */
export function isSchemaObject(
obj: schema.Object
): obj is Extract<schema.Object, { type: (typeof schemaObjects)[number] }> {
return isOneOfSchemaObjects(obj, schemaObjects);
}
const fieldObjects = ['field', 'enumerator'] as const;
/** Returns true if the value is a Field or Enumerator. */
export function isSchemaField(
field: schema.Field | schema.Enumerator
): field is Extract<schema.Field, { type: (typeof fieldObjects)[number] }> {
return field != null && 'type' in field && fieldObjects.includes(field.type);
}
/** Returns true if the value of the CstNode is a Token. */
export function isToken(node: [IToken] | [CstNode]): node is [IToken] {
return 'image' in node[0];
}
/**
* If parser.nodeLocationTracking is set, then read the location statistics
* from the available tokens. If tracking is 'none' then just return the
* existing data structure.
* */
export function appendLocationData<T extends Record<string, unknown>>(
data: T,
...tokens: IToken[]
): T {
const location = tokens.reduce((memo, token) => {
if (!token) return memo;
const {
endColumn = -Infinity,
endLine = -Infinity,
endOffset = -Infinity,
startColumn = Infinity,
startLine = Infinity,
startOffset = Infinity,
} = memo;
if (token.startLine != null && token.startLine < startLine)
memo.startLine = token.startLine;
if (token.startColumn != null && token.startColumn < startColumn)
memo.startColumn = token.startColumn;
if (token.startOffset != null && token.startOffset < startOffset)
memo.startOffset = token.startOffset;
if (token.endLine != null && token.endLine > endLine)
memo.endLine = token.endLine;
if (token.endColumn != null && token.endColumn > endColumn)
memo.endColumn = token.endColumn;
if (token.endOffset != null && token.endOffset > endOffset)
memo.endOffset = token.endOffset;
return memo;
}, {} as IToken);
return Object.assign(data, { location });
}

View File

@@ -0,0 +1,292 @@
import { CstNode, IToken } from '@chevrotain/types';
import * as Types from './getSchema';
import { appendLocationData, isToken } from './schemaUtils';
import { PrismaParser, defaultParser } from './parser';
import { ICstVisitor } from 'chevrotain';
/* eslint-disable @typescript-eslint/no-explicit-any */
type Class<T> = new (...args: any[]) => T;
export type PrismaVisitor = ICstVisitor<any, any>;
/* eslint-enable @typescript-eslint/no-explicit-any */
export const VisitorClassFactory = (
parser: PrismaParser
): Class<PrismaVisitor> => {
const BasePrismaVisitor = parser.getBaseCstVisitorConstructorWithDefaults();
return class PrismaVisitor extends BasePrismaVisitor {
constructor() {
super();
this.validateVisitor();
}
schema(ctx: CstNode & { list: CstNode[] }): Types.Schema {
const list = ctx.list?.map((item) => this.visit([item])) || [];
return { type: 'schema', list };
}
component(
ctx: CstNode & {
type: [IToken];
componentName: [IToken];
block: [CstNode];
}
): Types.Block {
const [type] = ctx.type;
const [name] = ctx.componentName;
const list = this.visit(ctx.block);
const data = (() => {
switch (type.image) {
case 'datasource':
return {
type: 'datasource',
name: name.image,
assignments: list,
} as const satisfies Types.Datasource;
case 'generator':
return {
type: 'generator',
name: name.image,
assignments: list,
} as const satisfies Types.Generator;
case 'model':
return {
type: 'model',
name: name.image,
properties: list,
} as const satisfies Types.Model;
case 'view':
return {
type: 'view',
name: name.image,
properties: list,
} as const satisfies Types.View;
case 'enum':
return {
type: 'enum',
name: name.image,
enumerators: list,
} as const satisfies Types.Enum;
case 'type':
return {
type: 'type',
name: name.image,
properties: list,
} as const satisfies Types.Type;
default:
throw new Error(`Unexpected block type: ${type}`);
}
})();
return this.maybeAppendLocationData(data, type, name);
}
break(): Types.Break {
return { type: 'break' };
}
comment(ctx: CstNode & { text: [IToken] }): Types.Comment {
const [comment] = ctx.text;
const data = {
type: 'comment',
text: comment.image,
} as const satisfies Types.Comment;
return this.maybeAppendLocationData(data, comment);
}
block(ctx: CstNode & { list: CstNode[] }): BlockList {
return ctx.list?.map((item) => this.visit([item]));
}
assignment(
ctx: CstNode & { assignmentName: [IToken]; assignmentValue: [CstNode] }
): Types.Assignment {
const value = this.visit(ctx.assignmentValue);
const [key] = ctx.assignmentName;
const data = {
type: 'assignment',
key: key.image,
value,
} as const satisfies Types.Assignment;
return this.maybeAppendLocationData(data, key);
}
field(
ctx: CstNode & {
fieldName: [IToken];
fieldType: [CstNode];
array: [IToken];
optional: [IToken];
attributeList: CstNode[];
comment: [IToken];
}
): Types.Field {
const fieldType = this.visit(ctx.fieldType);
const [name] = ctx.fieldName;
const attributes = ctx.attributeList?.map((item) => this.visit([item]));
const comment = ctx.comment?.[0]?.image;
const data = {
type: 'field',
name: name.image,
fieldType,
array: ctx.array != null,
optional: ctx.optional != null,
attributes,
comment,
} as const satisfies Types.Field;
return this.maybeAppendLocationData(
data,
name,
ctx.optional?.[0],
ctx.array?.[0]
);
}
fieldAttribute(
ctx: CstNode & {
fieldAttribute: [IToken];
groupName: [IToken];
attributeName: [IToken];
attributeArg: CstNode[];
}
): Types.Attr {
const [name] = ctx.attributeName;
const [group] = ctx.groupName || [{}];
const args = ctx.attributeArg?.map((attr) => this.visit(attr));
const data = {
type: 'attribute',
name: name.image,
kind: 'field',
group: group.image,
args,
} as const satisfies Types.Attr;
return this.maybeAppendLocationData(
data,
name,
...ctx.fieldAttribute,
group
);
}
blockAttribute(
ctx: CstNode & {
blockAttribute: [IToken];
groupName: [IToken];
attributeName: [IToken];
attributeArg: CstNode[];
}
): Types.Attr | null {
const [name] = ctx.attributeName;
const [group] = ctx.groupName || [{}];
const args = ctx.attributeArg?.map((attr) => this.visit(attr));
const data = {
type: 'attribute',
name: name.image,
kind: 'object',
group: group.image,
args,
} as const satisfies Types.Attr;
return this.maybeAppendLocationData(
data,
name,
...ctx.blockAttribute,
group
);
}
attributeArg(ctx: CstNode & { value: [CstNode] }): Types.AttributeArgument {
const value = this.visit(ctx.value);
return { type: 'attributeArgument', value };
}
func(
ctx: CstNode & {
funcName: [IToken];
value: CstNode[];
keyedArg: CstNode[];
}
): Types.Func {
const [name] = ctx.funcName;
const params = ctx.value?.map((item) => this.visit([item]));
const keyedParams = ctx.keyedArg?.map((item) => this.visit([item]));
const pars = (params || keyedParams) && [
...(params ?? []),
...(keyedParams ?? []),
];
const data = {
type: 'function',
name: name.image,
params: pars,
} as const satisfies Types.Func;
return this.maybeAppendLocationData(data, name);
}
array(ctx: CstNode & { value: CstNode[] }): Types.RelationArray {
const args = ctx.value?.map((item) => this.visit([item]));
return { type: 'array', args };
}
keyedArg(
ctx: CstNode & { keyName: [IToken]; value: [CstNode] }
): Types.KeyValue {
const [key] = ctx.keyName;
const value = this.visit(ctx.value);
const data = {
type: 'keyValue',
key: key.image,
value,
} as const satisfies Types.KeyValue;
return this.maybeAppendLocationData(data, key);
}
value(ctx: CstNode & { value: [IToken] | [CstNode] }): Types.Value {
if (isToken(ctx.value)) {
const [{ image }] = ctx.value;
return image;
}
return this.visit(ctx.value);
}
enum(
ctx: CstNode & {
enumName: [IToken];
attributeList: CstNode[];
comment: [IToken];
}
): Types.Enumerator {
const [name] = ctx.enumName;
const attributes = ctx.attributeList?.map((item) => this.visit([item]));
const comment = ctx.comment?.[0]?.image;
const data = {
type: 'enumerator',
name: name.image,
attributes,
comment,
} as const satisfies Types.Enumerator;
return this.maybeAppendLocationData(data, name);
}
maybeAppendLocationData<T extends Record<string, unknown>>(
data: T,
...tokens: IToken[]
): T {
if (parser.config.nodeLocationTracking === 'none') return data;
return appendLocationData(data, ...tokens);
}
};
};
type BlockList = Array<
| Types.Comment
| Types.Property
| Types.Attribute
| Types.Field
| Types.Enum
| Types.Assignment
| Types.Break
>;
export const DefaultVisitorClass = VisitorClassFactory(defaultParser);
export const defaultVisitor = new DefaultVisitorClass();