Giter Club home page Giter Club logo

prisma-ast's Introduction

Total Downloads npm package License

Buy Me A Coffee

@mrleebo/prisma-ast

This library uses an abstract syntax tree to parse schema.prisma files into an object in JavaScript. It also allows you to update your Prisma schema files using a Builder object pattern that is fully implemented in TypeScript.

It is similar to @prisma/sdk except that it preserves comments and model attributes. It also doesn't attempt to validate the correctness of the schema at all; the focus is instead on the ability to parse the schema into an object, manipulate it using JavaScript, and re-print the schema back to a file without losing information that isn't captured by other parsers.

It is probable that a future version of @prisma/sdk will render this library obsolete.

Install

npm install @mrleebo/prisma-ast

Examples

Produce a modified schema by building upon an existing schema

produceSchema(source: string, (builder: PrismaSchemaBuilder) => void, printOptions?: PrintOptions): string

produceSchema is the simplest way to interact with prisma-ast; you input your schema source and a producer function to produce modifications to it, and it will output the schema source with your modifications applied.

import { produceSchema } from '@mrleebo/prisma-ast';

const source = `
model User {
  id   Int    @id @default(autoincrement())
  name String @unique
}
`;

const output = produceSchema(source, (builder) => {
  builder
    .model('AppSetting')
    .field('key', 'String', [{ name: 'id' }])
    .field('value', 'Json');
});
model User {
  id   Int    @id @default(autoincrement())
  name String @unique
}

model AppSetting {
  key   String @id
  value Json
}

For more information about what the builder can do, check out the PrismaSchemaBuilder class.

PrismaSchemaBuilder

The produceSchema() utility will construct a builder for you, but you can also create your own instance, which may be useful for more interactive use-cases.

import { createPrismaSchemaBuilder } from '@mrleebo/prisma-ast';

const builder = createPrismaSchemaBuilder();

builder
  .model('User')
  .field('id', 'Int')
  .attribute('id')
  .attribute('default', [{ name: 'autoincrement' }])
  .field('name', 'String')
  .attribute('unique')
  .break()
  .comment('this is a comment')
  .blockAttribute('index', ['name']);

const output = builder.print();
model User {
  id   Int @id @default(autoincrement())
  name String @unique

  // this is a comment
  @@index([name])
}

Query the prisma schema for specific objects

The builder can also help you find matching objects in the schema based on name (by string or RegExp) or parent context. You can use this to write tests against your schema, or find fields that don't match a naming convention, for example.

const source = `
  model Product {
    id     String  @id @default(auto()) @map("_id") @db.ObjectId
    name   String
    photos Photo[]
  }
`

const builder = createPrismaSchemaBuilder(source);

const product = builder.findByType('model', { name: 'Product' });
expect(product).toHaveProperty('name', 'Product');

const id = builder.findByType('field', {
  name: 'id',
  within: product?.properties,
});
expect(id).toHaveProperty('name', 'id');

const map = builder.findByType('attribute', {
  name: 'map',
  within: id?.attributes,
});
expect(map).toHaveProperty('name', 'map');

Re-sort the schema

prisma-ast can sort the schema for you. The default sort order is ['generator', 'datasource', 'model', 'enum'] and will sort objects of the same type alphabetically.

print(options?: {
  sort: boolean,
  locales?: string | string[],
  sortOrder?: Array<'generator' | 'datasource' | 'model' | 'enum'>
})

You can optionally set your own sort order, or change the locale used by the sort.

// sort with default parameters
builder.print({ sort: true });

// sort with options
builder.print({
  sort: true,
  locales: 'en-US',
  sortOrder: ['datasource', 'generator', 'model', 'enum'],
});

Need More SchemaBuilder Code snippets?

There is a lot that you can do with the schema builder. There are additional sample references available for you to explore.

Configuration Options

prisma-ast uses lilconfig to read configuration options which can be located in any of the following files, and in several other variations (see the complete list of search paths):

  • "prisma-ast" in package.json
  • .prisma-astrc
  • .prisma-astrc.json
  • .prisma-astrc.js
  • .config/.prisma-astrc

Configuration options are:

Option Description Default Value
parser.nodeTrackingLocation Include the token locations of CST Nodes in the output schema.
Disabled by default because it can impact parsing performance.
Possible values are "none", "onlyOffset", and "full".
"none"

Example Custom Configuration

Here is an example of how you can customize your configuration options in package.json.

{
  "prisma-ast": {
    "parser": {
      "nodeTrackingLocation": "full"
    }
  }
}

Underlying utility functions

The produceSchema and createPrismaSchemaBuilder functions are intended to be your interface for interacting with the prisma schema, but you can also get direct access to the AST representation if you need to edit the schema for more advanced usages that aren't covered by the methods above.

Parse a schema.prisma file into an AST object

The shape of the AST is not fully documented, and it is more likely to change than the builder API.

import { getSchema } from '@mrleebo/prisma-ast';

const source = `
model User {
  id   Int    @id @default(autoincrement())
  name String @unique
}
`;

const schema = getSchema(source);

Print a schema AST back out as a string

This is what builder.print() calls internally, and is what you'd use to print if you called getSchema().

import { printSchema } from '@mrleebo/prisma-ast';

const source = printSchema(schema);

You can optionally re-sort the schema. The default sort order is ['generator', 'datasource', 'model', 'enum'], and objects with the same type are sorted alphabetically, but the sort order can be overridden.

const source = printSchema(schema, {
  sort: true,
  locales: 'en-US',
  sortOrder: ['datasource', 'generator', 'model', 'enum'],
});

prisma-ast's People

Contributors

gogoout avatar haringat avatar juanm04 avatar maxh avatar mrleebo avatar woodensail avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

prisma-ast's Issues

MismatchedTokenException if certain keywords are used in the generator name

My generator is named "prisma-model-generator", and in my prisma.schema-file I had it plugged in like this:

generator prisma-model-generator {
    provider         = "node ./dist/apps/prisma-model-generator/src/generator.js"
    fileNamingStyle  = "kebab"
    classNamingStyle = "pascal"
    output           = "./generated/"
}

This made prisma-ast quite disagreeable, which I quickly discovered was due to the keywords model and generator being used in the name.

This was the first error message:
{"jsonrpc":"2.0","error":{"code":-32000,"message":"Expecting --> '{' <-- but found --> 'class' <--","data":{"stack":"MismatchedTokenException: Expecting --> '{' <-- but found --> 'class' <--\n at RecognizerEngine.consumeInternalError

I then tried removing model from the name, which got me this instead:
{"jsonrpc":"2.0","error":{"code":-32000,"message":"Expecting --> '{' <-- but found --> 'generator' <--","data":{"stack":"MismatchedTokenException: Expecting --> '{' <-- but found --> 'generator' <--\n at RecognizerEngine.consumeInternalError

Changing it to this bypassed the problem completely:

generator foobar {
    provider         = "node ./dist/apps/prisma-model-generator/src/generator.js"
    fileNamingStyle  = "kebab"
    classNamingStyle = "pascal"
    output           = "./generated/"
}

So it's an easy workaround, but thought you'd still might want to look into it :)

getSchema doesn't produce break-nodes

Hi! Using your lib to enhance schema keeping it almost same as original. You stated that "Comments and Line breaks are also parsed", and by typings I expect { type: 'break' } in model.properties, but there's everything but it.

Am I missing something, mb it's configurable, bug or that's intended?

[thanx for nice tool btw ๐Ÿ’œ]

Doesn't extend existing model

Hey.
I've been trying to modify my existing schema using your lib but it seems that my every change produces a new model entry even if the model already exists

This is my code

 const schemaBuilder = createPrismaSchemaBuilder(schemaAsString); // in schema there is TaskScript model

 schemaBuilder.model("TaskScript").field("createdAt2", "DateTime").attribute("default", [ { name: "now" }]);
 schemaBuilder.model("TaskScript").field("updatedAt2", "DateTime").attribute("updatedAt");

My output is

image

so in the output I have three definitions of the TaskScript model. Do you have any idea why this is hapenning?

Bug with db.ObjectId

Hello, first of all many thanks for the beautiful package!

When i add db.ObjectId to a field, this works, but when i run the builder repeatedly, this field always duplicates itself.

builder
        .model(input.name)
        .field("id", "String")
        .attribute("id")
        .attribute("default", [{ name: "auto" }])
        .attribute("map", [`"_id"`])
        .attribute("db.ObjectId");

First run:
model Test { id String @id @default(auto()) @map("_id") @db.ObjectId }

Second run:
model Test { id String @id @default(auto()) @map("_id") @db.ObjectId @db.ObjectId }

Thirt run:
model Test { id String @id @default(auto()) @map("_id") @db.ObjectId @db.ObjectId @db.ObjectId }

As workaround i replace double @db.ObjectId with
`const output = builder
.print()
.replace("@db.ObjectId @db.ObjectId", "@db.ObjectId");``

But this cannot be best practice.

This is probably due to the dot in db.ObjectId, as test.test has the same error behaviour.

`getSchema` - model attributes are not parsed

Example model:

model course {
...model fields

 @@index([club_id], map: "club_id")
}

when I use the getSchema method:

const source = readFileSync("path/to/schema.prisma", { encoding: "utf8" });
const schema = getSchema(source);

I don't get the models attributes, like I get the attributes for fields

Throws when passing an empty array into an attribute

Hi there! I've been trying to write a PR for this but haven't quite figured out where the issues lays, so I thought I should write an issue while it is fresh and hopefully I can rubber duck myself into an answer.

datasource db {
  provider = "postgres"
  url      = env("DATABASE_URL")
}

generator client {
  provider      = "prisma-client-js"
  binaryTargets = "native"
}

model UserProfile {
  userID String @id @unique

  /// A one liner
  bio String?

  /// Hrefs which show under the user
  links String[] @default([])
}

Will throw due to links String[] @default([]). It throws in:

  this.OPTION(() => {
      this.CONSUME(lexer.LRound);
      this.MANY_SEP({
        SEP: lexer.Comma,
        DEF: () => {
          this.SUBRULE(this.attributeArg);
        },
      });
      this.CONSUME(lexer.RRound);
    });

Which feels a bit odd because this.attributeArg eventually includes this.array which should be able to handle the empty array judging by the JSON parser in the playground which has the same definition code.

Converting it to links String[] @default([""]) passes, but would have different runtime behaviours.

Things I've tried:

  • Defining an 'empty array' parse rule for it
  • Adding array specifically to the attributeArg fn

Thank you!

Hello,

I just wanted to thank you for this awesomeness. It enabled me to fix an issue in my library for good.

Hope I could contribute here in the future!

Sorry for the non-issue, as there are no discussions.

feature request: Include source code location for each node

I'm using your library to power https://github.com/loop-payments/prisma-lint. It's working great -- thanks!

I'd like to show RuboCop-like errors with the exact source text embedded, maybe something like this:

https://github.com/rubocop/rubocop/blob/c3c9b42b876d08d83738d57203c0baf9b4b0b865/spec/rubocop/cop/style/unless_else_spec.rb#L7-L8

Having location within the source code for each node would be useful. Perhaps something like what ESLint does:

https://github.com/eslint/eslint/blob/e0cf0d86d985ed2b2f901dd9aab5ccd2fff062ad/tests/lib/rules/array-callback-return.js#L458-L461

What do you think?

Schema Builder API

It would be nice to have a schema builder interface to chain multiple edits together, something like

createSchemaBuilder()
  .addModel("Project", fields)
  .addEnum("Role", enumerators)
  .print()

Support for @map on enum fields

An enum field can have a @map attribute that defines an explicit value:

https://www.prisma.io/docs/orm/prisma-schema/data-model/database-mapping#map-enum-names-and-values

For example:

enum GradeLevel {
  KINDERGARTEN   @map("kindergarten")
  FIRST          @map("first")
  SECOND         @map("second")
  THIRD          @map("third")
  FOURTH         @map("fourth")
  FIFTH          @map("fifth")
  SIXTH          @map("sixth")
  SEVENTH        @map("seventh")
  EIGHTH         @map("eighth")
  NINTH          @map("ninth")
  TENTH          @map("tenth")
  ELEVENTH       @map("eleventh")
  TWELFTH        @map("twelfth")
  THIRTEEN       @map("thirteen")
  POST_SECONDARY @map("post_secondary")
  OTHER          @map("other")
}

Could this be included?

@@map in Enums can not be parsed properly

It seem that the parser/lexer/tokenizer (I dunno) does not support @@map in Enums. As far as I can tell - this is perfectly valid:

https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#map-1

import { getSchema } from '@mrleebo/prisma-ast'

const source = `
enum MembershipRole {
  OWNER
  ADMIN
  USER

  @@map("membership_role")
}
`

const schema = getSchema(source)
        throw this.SAVE_ERROR(new exceptions_public_1.NoViableAltException(errMsg, this.LA(1), previousToken));
                              ^

NoViableAltException: Expecting: one of these possible Token sequences:
  1. [Comment]
  2. [Identifier, '=', StringLiteral]
  3. [Identifier, '=', NumberLiteral]
  4. [Identifier, '=', '[']
  5. [Identifier, '=', Identifier]
  6. [Identifier, '=', True]
  7. [Identifier, '=', False]
  8. [Identifier, '=', Null]
  9. ['@@']
  10. ['@']
  11. [Identifier, StringLiteral]
  12. [Identifier, NumberLiteral]
  13. [Identifier, '[']
  14. [Identifier, Identifier]
  15. [Identifier, True]
  16. [Identifier, False]
  17. [Identifier, Null]
  18. [Identifier]
  19. [Identifier, '=', StringLiteral]
  20. [Identifier, '=', NumberLiteral]
  21. [Identifier, '=', '[']
  22. [Identifier, '=', Identifier]
  23. [Identifier, '=', True]
  24. [Identifier, '=', False]
  25. [Identifier, '=', Null]
  26. [LineBreak, LineBreak]
  27. [LineBreak]
but found: '@@'
    at PrismaParser.ErrorHandler.raiseNoAltException (/Users/tester/node_modules/chevrotain/lib/src/parse/parser/traits/error_handler.js:80:31)
    at PrismaParser.RecognizerEngine.orInternal (/Users//tester/node_modules/chevrotain/lib/src/parse/parser/traits/recognizer_engine.js:402:14)
    at PrismaParser.RecognizerApi.OR (/Users/tester/node_modules/chevrotain/lib/src/parse/parser/traits/recognizer_api.js:133:21)
    at PrismaParser.<anonymous> (/Users/tester/node_modules/@mrleebo/prisma-ast/dist/prisma-ast.cjs.development.js:340:15)
    at PrismaParser.RecognizerEngine.doSingleRepetition (/Users/tester/node_modules/chevrotain/lib/src/parse/parser/traits/recognizer_engine.js:385:16)
    at PrismaParser.RecognizerEngine.manyInternalLogic (/Users/tester/node_modules/chevrotain/lib/src/parse/parser/traits/recognizer_engine.js:318:29)
    at PrismaParser.RecognizerEngine.manyInternal (/Users/tester/node_modules/chevrotain/lib/src/parse/parser/traits/recognizer_engine.js:295:21)
    at PrismaParser.RecognizerApi.MANY (/Users/tester/node_modules/chevrotain/lib/src/parse/parser/traits/recognizer_api.js:163:14)
    at PrismaParser.<anonymous> (/Users/tester/node_modules/@mrleebo/prisma-ast/dist/prisma-ast.cjs.development.js:339:13)
    at PrismaParser.invokeRuleWithTry (/Users/tester/node_modules/chevrotain/lib/src/parse/parser/traits/recognizer_engine.js:112:26) {
  token: {
    image: '@@',
    startOffset: 49,
    endOffset: 50,
    startLine: 7,
    endLine: 7,
    startColumn: 3,
    endColumn: 4,
    tokenTypeIdx: 15,
    tokenType: {
      name: 'ModelAttribute',
      PATTERN: /@@/,
      CATEGORIES: [
        {
          name: 'Attribute',
          PATTERN: /NOT_APPLICABLE/,
          tokenTypeIdx: 14,
          CATEGORIES: [],
          categoryMatches: [ 15, 16 ],
          categoryMatchesMap: { '15': true, '16': true },
          isParent: true
        }
      ],
      tokenTypeIdx: 15,
      categoryMatches: [],
      categoryMatchesMap: {},
      isParent: false,
      LABEL: "'@@'"
    }
  },
  resyncedTokens: [],
  previousToken: {
    image: '\n',
    startOffset: 46,
    endOffset: 46,
    startLine: 6,
    endLine: 6,
    startColumn: 1,
    endColumn: 1,
    tokenTypeIdx: 32,
    tokenType: {
      name: 'LineBreak',
      PATTERN: /\n|\r\n/,
      tokenTypeIdx: 32,
      CATEGORIES: [],
      categoryMatches: [],
      categoryMatchesMap: {},
      isParent: false,
      LABEL: 'LineBreak',
      LINE_BREAKS: true
    }
  },
  context: {
    ruleStack: [ 'schema', 'component', 'block' ],
    ruleOccurrenceStack: [ 0, 0, 0 ]
  }
}

Renaming a model attribute or deleting an attribute and creating a new attribute with the same args of the deleted one

I am trying to rename an attribute, but instead, it renames my model:

builder
  .model(model.name)
  .blockAttribute(ID_ATTRIBUTE_NAME)
  .then<ModelAttribute>((attr) => {
      attr.name = UNIQUE_ATTRIBUTE_NAME;
   });

I tried another way, which is finding the attribute I want to rename, taking its args and creating from it a new attribute, from the type (name) I want and then delete the attribute that I don't want:

const argsFromIdAttribute = (
        modelIdAttribute.args[0].value as RelationArray
      ).args;

 // change the @@id attribute to @@unique attribute
      builder
        .model(model.name)
        .blockAttribute(UNIQUE_ATTRIBUTE_NAME, argsFromIdAttribute);

// remove the @@id attribute from the model - I know it will not work because I need to remove a block attribute, but the 
     // lib doesn't have it so consider it as a pseudo code
      builder.model(model.name).removeAttribute(ID_ATTRIBUTE_NAME);

In this case, I am getting the following error: Subject must be a prisma field!

What am I doing wrong? Does this library support what I am trying to do?

Trailing comments not handled properly

First off, this library is awesome, thanks for creating it! It's been really useful for customizing our Prisma schema formatting beyond what prisma format can do. I did run into this edge case with trailing comments though, I wonder how hard it would be to fix.

Summary

Trailing comments at the end of a line get forced onto the next line when parsing and then printing a schema.

Current behavior

The following code:

import { getSchema, printSchema } from '@mrleebo/prisma-ast';

const source = `
enum Role {
  ADMIN
  OWNER // similar to ADMIN, but can delete the project
  MEMBER
  USER // deprecated
}
`;

const schema = getSchema(source);
const output = printSchema(schema);
console.log(output);

outputs:

enum Role {
  ADMIN
  OWNER
  // similar to ADMIN, but can delete the project
  MEMBER
  USER
  // deprecated
}

Expected behavior

I would have expected that code to output one of these:

// Option 1: preserve trailing comments at end of line
enum Role {
  ADMIN
  OWNER // similar to ADMIN, but can delete the project
  MEMBER
  USER // deprecated
}

// Option 2: move trailing comments to the preceding line
enum Role {
  ADMIN
  // similar to ADMIN, but can delete the project
  OWNER
  MEMBER
  // deprecated
  USER
}

MismatchedTokenException: Expecting --> '}' <-- but found --> 'model' <--

Hi, thanks for the great work. When I parse my schema with the parser, it will throw this error because we have model as one of our table's field name.

To reproduce, just parse this schema:

// https://www.prisma.io/docs/concepts/components/prisma-schema
// added some fields to test keyword ambiguous

datasource db {
  url      = env("DATABASE_URL")
  provider = "postgresql"
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  email     String   @unique
  name      String?
  role      Role     @default(USER)
  posts     Post[]
}

model Post {
  id         Int      @id @default(autoincrement())
  createdAt  DateTime @default(now())
  updatedAt  DateTime @updatedAt
  published  Boolean  @default(false)
  title      String   @db.VarChar(255)
  author     User?    @relation(fields: [authorId], references: [id])
  authorId   Int?
  // keyword test
  model      String
  generator  String
  datasource String
  enum       String
}

enum Role {
  USER
  ADMIN
}

getSchema- Empty comments are not parsed correctly

Empty comments are not closed correctly and take the next node as text. Schema is parsed with function getSchema.
Empty comments are added to the schema:
image

Instead of an empty string the next field is taken as comment text and the field is not added as property:
image

Issue with documentation example

This example;

builder.model('User')
  .field('id', 'Int')
  .attribute('id')
  .attribute('default', [{ function: 'autoincrement' }])

gives me an error.

I reproduced the issue in a clean Replit Node Repl;

import { createPrismaSchemaBuilder } from '@mrleebo/prisma-ast'

const builder = createPrismaSchemaBuilder()

builder.model('User')
  .field('id', 'Int')
  .attribute('id')
  .attribute('default', [{ function: 'autoincrement' }])
  .field('name', 'String')
  .attribute('unique')
  .break()
  .comment("this is a comment")
  .blockAttribute('index', ['name'])

const output = builder.print()

will result in:

/home/runner/UnfoldedClosedVisitor/node_modules/@mrleebo/prisma-ast/dist/prisma-ast.cjs.development.js:1336
          params: (_arg$function$map = (_arg$function = arg["function"]) == null ? void 0 : _arg$function.map(mapArg)) != null ? _arg$function$map : []
                                                                                                          ^

TypeError: _arg$function.map is not a function
    at mapArg (/home/runner/UnfoldedClosedVisitor/node_modules/@mrleebo/prisma-ast/dist/prisma-ast.cjs.development.js:1336:107)
    at /home/runner/UnfoldedClosedVisitor/node_modules/@mrleebo/prisma-ast/dist/prisma-ast.cjs.development.js:1343:18
    at Array.map (<anonymous>)
    at ConcretePrismaSchemaBuilder.attribute (/home/runner/UnfoldedClosedVisitor/node_modules/@mrleebo/prisma-ast/dist/prisma-ast.cjs.development.js:1340:50)
    at file:///home/runner/UnfoldedClosedVisitor/index.js:8:4
    at ModuleJob.run (node:internal/modules/esm/module_job:193:25)
    at async Promise.all (index 0)
    at async ESMLoader.import (node:internal/modules/esm/loader:530:24)

Any ideas?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.