One of the inixiative building blocks. Stop rediscovering what you need to build. See all the projects.
Reads the Prisma client you already generate and gives you its models, fields, relations and foreign keys as one plain object you can use at runtime. You don't add a Prisma generator or parse the DMMF yourself. The map keys everything by its Prisma name and records the database name wherever @map / @@map changes it, so code that writes SQL resolves identifiers through tableName / columnName / storedValue instead of guessing. @inixiative/permissions walks relations through this map, and @inixiative/json-rules reads it as a field map.
@inixiative/prisma-map builds a PrismaMap object that includes:
- Models and fields
- Field kinds (
scalar,enum,object) - Relation FK direction (
fromFields,toFields) - Model DB table name (
@@map) asdbName - Field DB column name (
@map) asdbName - Enum values (ordered), with stored DB values (
@mapon members) asvalueDbNames - Field default (
@default) asdefault:{ kind: 'literal', value }for a value stored as written (enum member, string, number, boolean, list; a Json default parsed to its value; BigInt/Decimal kept as exact text),{ kind: 'generated', expression }for a function the database or Prisma evaluates (now(),uuid(),dbgenerated(...),autoincrement()) or a Bytes value; generated defaults are never injected
It supports both Prisma client layouts:
- Prisma v7: parse generated
internal/class.ts - Prisma v6: parse generated
schema.prisma
bun add @inixiative/prisma-mapor
npm i @inixiative/prisma-mapimport { buildPrismaMapV7 } from '@inixiative/prisma-map';
const map = buildPrismaMapV7();
// or: buildPrismaMapV7('/absolute/path/to/generated/client')
console.log(map.User.fields.posts);import { buildPrismaMapV6 } from '@inixiative/prisma-map';
const map = buildPrismaMapV6();
// or: buildPrismaMapV6('/absolute/path/to/generated/client')
console.log(map.Post.fields.author);The library returns:
type PrismaMap = Record<string, {
dbName: string | null;
fields: Record<string, ScalarField | EnumField | RelationField>;
}>;{
"kind": "scalar",
"type": "String",
"isRequired": true,
"isList": false,
"isId": false
}{
"kind": "enum",
"type": "UserRole",
"isRequired": true,
"isList": false,
"values": ["ADMIN", "USER", "GUEST"]
}{
"kind": "object",
"type": "User",
"isList": false,
"isRequired": true,
"relationName": "AuthoredPosts",
"fromFields": ["authorId"],
"toFields": ["id"]
}fromFields/toFields are empty arrays on back-relations.
Given:
enum Role {
ADMIN
USER
}
model User {
id String @id
role Role
posts Post[]
@@map("users")
}
model Post {
id String @id
authorId String
author User @relation("AuthoredPosts", fields: [authorId], references: [id])
}A representative map:
{
"User": {
"dbName": "users",
"fields": {
"id": {
"kind": "scalar",
"type": "String",
"isRequired": true,
"isList": false,
"isId": true
},
"role": {
"kind": "enum",
"type": "Role",
"isRequired": true,
"isList": false,
"values": ["ADMIN", "USER"]
},
"posts": {
"kind": "object",
"type": "Post",
"isList": true,
"isRequired": true,
"fromFields": [],
"toFields": []
}
}
},
"Post": {
"dbName": null,
"fields": {
"author": {
"kind": "object",
"type": "User",
"isList": false,
"isRequired": true,
"relationName": "AuthoredPosts",
"fromFields": ["authorId"],
"toFields": ["id"]
}
}
}
}Root exports:
import {
buildPrismaMapV6,
buildPrismaMapV7,
getColumns,
getRelations,
pickColumns,
relationForeignKey,
parseTagClasses,
type PrismaMap,
type ModelEntry,
type ModelField,
type ScalarField,
type EnumField,
type RelationField,
type IndexEntry,
type RelationInfo,
type Identifier,
type Annotations,
} from '@inixiative/prisma-map';Version-specific exports:
import { buildPrismaMapV7, parseRuntimeDataModel, parseInlineSchema, parseRelationFks, parseEnumValues, parseSchemaStructure } from '@inixiative/prisma-map/v7';
import { buildPrismaMapV6, parseSchemaText, buildFromSchemaFile } from '@inixiative/prisma-map/v6';getColumns(map, modelName) lists a model's column field names (scalar and enum
fields). pickColumns(map, modelName, row) keeps only those keys of a row, so a
write result carrying included relations reduces to the record itself.
import { getColumns, pickColumns } from '@inixiative/prisma-map';
getColumns(map, 'User');
// ['id', 'role']
pickColumns(map, 'User', { id: 'u1', role: 'ADMIN', posts: [{ id: 'p1' }] });
// { id: 'u1', role: 'ADMIN' }getRelations(map, modelName) lists a model's relation fields with their foreign
keys collapsed for lookup; relationForeignKey(field) does the collapse alone.
import { getRelations } from '@inixiative/prisma-map';
getRelations(map, 'Inquiry');
// [
// { relationName: 'organization', targetModel: 'Organization', isList: false,
// foreignKey: { id: 'organizationId' } },
// { relationName: 'parent', targetModel: 'Inquiry', isList: false,
// foreignKey: { id: 'parentId' }, annotations: { tree: { parent: true } } },
// { relationName: 'children', targetModel: 'Inquiry', isList: true, foreignKey: null },
// ]foreignKey is a bare field name for a single same-named pair, a
{ referencedField: localField } map otherwise, and null for back-relations.
Functions are string-keyed. Wrap them with your generated ModelName types if
you want typed accessors.
Prisma rejects custom attributes, but preserves /// doc comments: into the
generated client's inlineSchema (v7) and the schema.prisma it copies (v6). prisma-map parses a
small, forward-looking DSL out of those comments, the same way for both, and records it under
annotations, without assigning the tags any meaning.
model Category {
id String @id
parentId String?
/// @tree(parent: true)
parent Category? @relation("tree", fields: [parentId], references: [id])
children Category[] @relation("tree")
/// @search(fuzzy: true)
@@index([name])
}map.Category.fields.parent.annotations; // { tree: { parent: true } }
map.Category.indexes;
// [{ kind: 'index', fields: ['name'], annotations: { search: { fuzzy: true } } }]- Grammar:
@<tagClass>(<key>: <value>, …); multiple tag classes per line allowed. - Values:
true/false→ boolean, numerics → number,"quoted"/bare words → string,[a, b]→ array. - Placement: a
///run binds to the declaration directly below it: a model, a field, or an index (@@index/@@unique/@@id/@@fulltext). A blank line or a plain//breaks the binding. - prisma-map only records the bag; what a tag class means (e.g.
tree.parent→ "don't auto-recurse up this self-relation") is the consumer's to decide.
If you omit the path, the library walks upward from process.cwd() and checks common generated client locations:
node_modules/.prisma/clientnode_modules/@prisma/clientprisma/generated/clientsrc/generated/clientsrc/generated/prismagenerated/client
v7 detection requires internal/class.ts.
v6 detection requires schema.prisma and excludes directories that also have internal/class.ts.
- Build rule-engine metadata for JSON rules
- Resolve template/package dependency graphs via relation edges
- Generate guardrails (for example, "entity X must include relation Y")
- Build field-level introspection UIs
map.X.fields.y is a ModelField union, so narrow it first. Relation fields have
no column and the resolvers reject them at compile time:
import {
columnName,
hasColumn,
isEnumField,
storedValue,
tableName,
type DbIdentifier,
type DbValue,
} from '@inixiative/prisma-map';
const model = map.BrandMissions;
tableName(model, 'BrandMissions'); // '@@map' if set, else 'BrandMissions'
const createdAt = model.fields.createdAt;
if (hasColumn(createdAt)) {
columnName(createdAt, 'createdAt'); // 'created_at' via '@map'
}
const typeName = model.fields.typeName;
if (isEnumField(typeName)) {
storedValue(typeName, 'SOCIAL_POST'); // 'Social Post' via '@map' on the member
storedValue(typeName, 'UNMAPPED'); // 'UNMAPPED' — no '@map', the name is the value
}The resolvers return branded DbIdentifier / DbValue rather than bare string. Both
surfaces are strings, so a mix-up otherwise produces valid SQL that silently matches no
rows. Type a SQL builder's parameters as the brands and a raw Prisma name becomes a
compile error:
const eq = (column: DbIdentifier, value: DbValue) => `${column} = '${value}'`;
eq('missionTypeName', 'SOCIAL_POST'); // compile error — raw Prisma names rejected- Every key in the map is the Prisma name:
map.User,map.User.fields.createdAt, and the entries ofvalues.dbName/valueDbNamesare the only crossings to the physical DB surface, and both are sparse: absent means the Prisma name IS the DB identifier. Resolve them withtableName/columnName/storedValuerather than open-coding?? name. A missed fallback fails silently (comparing against an unmapped enum member selects zero rows instead of erroring). @@mapon a model is emitted asModelEntry.dbName;@mapon a scalar/enum field as the field'sdbName;@mapon enum members as the field'svalueDbNames.- Relation fields never carry
dbName; they have no column. - Native DB type annotations (for example
@db.Text) are not emitted. relationNameis included when available in Prisma runtime metadata.
bun run test
bun run typecheck
bun run check
bun run buildMIT