Skip to main content

Built and signed on GitHub Actions

Library to write out the contents of a JavaScript object structure to the file system in a granular directory structure.

This package works with Node.js, Deno, Bun
This package works with Node.js
This package works with Deno
This package works with Bun
JSR Score
100%
Published
5 hours ago (0.1.4)

Object-To-Directory

This library is the reverse of @scroogieboy/directory-to-object. It makes it easy to write out the contents of a JavaScript object structure to the file system in a granular fashion: instead of writing a single JSON, YAML or other format file, the data is written to a directory, with properties written to various formats (or directories) based on configuration.

Concepts

The central concept of this library is the value storage handler: an object implementing interface ValueStorageHandler whose responsibility it is to store a value in the file system. There are value storage handlers that write to files (e.g., text files, JSON files, etc.) and there are value storage handlers that write objects/arrays to directories by storing their individual properties/items within each directory.

The value storage handlers can be created in a fluent fashion using the Handlers singleton. For example, in the Deno REPL:

> import * as otd from "jsr:@scroogieboy/object-to-directory";
undefined
> let x = otd.Handlers.textFile().whenPathMatches("**/*foo*").withName("foo handler")
undefined
> x.name
"foo handler"

Value storage handlers have a simple interface: a name property, a method to determine if a value can be stored using the handler and a method to perform the storage.

export interface ValueStorageHandler {
  readonly name: string;

  canStoreValue(
    pathInSource: string,
    destinationUrl: URL,
    value: unknown,
  ): boolean;

  storeValue(
    pathInSource: string,
    destinationUrl: URL,
    value: unknown,
    options?: Readonly<ValueStorageHandlerOptions>,
  ): Promise<void>;
}

At runtime, as each property of an object to store is visited, the configured handlers are queried in order -- calling canStoreValue on each until one responds true. The property is then stored by calling the storeValue on the first handler that responded to the canStoreValue query.

See the examples below for more concrete code that makes use of handler configuration.

API

The storeObjectToDirectory function stores a JavaScript object to the file system as a directory. It relies on value storage handlers to guide how the various properties in the input object and their descendents are written to the file system.

The Handlers singleton variable builder singleton is a convenient way to build handlers for storeObjectToDirectory. See the HandlerBuilder interface for a description of the various handlers that can be created through the builder.

Examples

Write the OpenAPI "pet store" spec in a decomposed directory structure, then read it back using @scroogieboy/directory-to-object

This example uses a small set of handlers set up to define output format choices for specific paths in the spec.

It also makes use of the property name encoder option to map characters that are forbidden in the file system to more aesthetic choices (mapping the forward slash to a full-width solidus).

Finally, it reads the directory structure back into memory using the @scroogieboy/directory-to-object package and verifies that the specs match.

import { assertEquals } from "@std/assert";
import {
  Handlers,
  storeObjectToDirectory,
} from "@scroogieboy/object-to-directory";
import { loadObjectFromDirectory } from "@scroogieboy/directory-to-object";
import petstore from "./petstore.json" with { type: "json" };

const destinationUrl = new URL(import.meta.resolve("../tmp/petstore"));

const handlers = [
  // Write the "openapi" value as its own file.
  Handlers.textFile().whenPathMatches("/openapi"),
  // These are the path patterns we want to write out as JSON files.
  Handlers.jsonFile().whenPathMatchesSome([
    "/components/schemas/*",
    "/info",
    "/paths/*",
    "/servers",
  ]),
  // Any remaining objects will be written as directories.
];

// Clean up the destination directory, if it exists.
try {
  await Deno.remove(destinationUrl, { recursive: true });
} catch (e) {
  if (!(e instanceof Deno.errors.NotFound)) {
    throw e;
  }
}

// Let's use a little Unicode trickery to make the paths look pretty: replace "/" with
// the full-width solidus character, which is allowed in the file system.
await storeObjectToDirectory(destinationUrl, petstore, handlers, {
  strict: true,
  propertyNameEncoder: (name) => name.replaceAll("/", "/"),
});

// Use directory-to-object to load the OpenAPI spec back in, with the corresponding name decoding.
const loadedPetstore = await loadObjectFromDirectory(destinationUrl, {
  propertyNameDecoder: (name: string) => name.replaceAll("/", "/"),
});

// The specs should be identical 😀.
assertEquals(loadedPetstore, petstore);

The full example source code can be found in the examples directory.

Running this program will write the OpenAPI spec decomposed into multiple files:

tmp
└── petstore
    ├── components
    │   └── schemas
    │       ├── Error.json
    │       ├── Pet.json
    │       └── Pets.json
    ├── info.json
    ├── openapi.txt
    ├── paths
    │   ├── /pets.json
    │   └── /pets/{petId}.json
    └── servers.json

Unpack the vulnerabilities from a Trivy scan JSON file

This is a somewhat contrived example that makes use of nested directory handlers to unpack the contents of a Trivy container image scan (e.g., the Deno distroless Docker image) into a directory structure and write the vulnerabilities as a CSV file.

We'll use the json-2-csv NPM package for the actual CSV file writing.

import { json2csv } from "json-2-csv";
import {
  Handlers,
  storeObjectToDirectory,
} from "@scroogieboy/object-to-directory";
import trivyOutput from "./trivy_output.json" with { type: "json" };

const destinationUrl = new URL(import.meta.resolve("../tmp/trivy_output"));

// Let's roll our own CSV handler.
const csvHandler = Handlers.customFile({
  serializer: (v) => Array.isArray(v) ? json2csv(v) : "",
  extension: ".csv",
  name: "CSV Handler",
});

// Set up the handlers for the "/Results" path -- we want to treat this path differently.
const resultsHandlers = [
  csvHandler.whenIsArray(),
  Handlers.textFile(),
];

const handlers = [
  // Plain-text properties go to .txt files.
  Handlers.textFile(),
  // The "Results" array property is written as a directory containing CSV files of vulnerabilities.
  Handlers.arrayToDirectory({
    keyProperty: "Target",
    handlers: resultsHandlers,
  }).whenPathMatches("/Results"),
  // Default to writing any other properties as JSON files.
  Handlers.jsonFile(),
];

// Clean up the destination directory, if it exists.
try {
  await Deno.remove(destinationUrl, { recursive: true });
} catch (e) {
  if (!(e instanceof Deno.errors.NotFound)) {
    throw e;
  }
}

// Let's write out a directory structure with the Trivy output broken out, including the
// vulnerabilities as a CSV file.
await storeObjectToDirectory(destinationUrl, trivyOutput, handlers, {
  strict: true,
});
tmp
└── trivy_output
    ├── ArtifactName.txt
    ├── ArtifactType.txt
    ├── CreatedAt.txt
    ├── Metadata.json
    ├── Results
    │   └── denoland%2Fdeno:distroless (debian 12.8)
    │       ├── Class.txt
    │       ├── Target.txt
    │       ├── Type.txt
    │       └── Vulnerabilities.csv
    └── SchemaVersion.json
Built and signed on
GitHub Actions
View transparency log

Add Package

deno add jsr:@scroogieboy/object-to-directory

Import symbol

import * as object_to_directory from "@scroogieboy/object-to-directory";

---- OR ----

Import directly with a jsr specifier

import * as object_to_directory from "jsr:@scroogieboy/object-to-directory";

Add Package

npx jsr add @scroogieboy/object-to-directory

Import symbol

import * as object_to_directory from "@scroogieboy/object-to-directory";

Add Package

yarn dlx jsr add @scroogieboy/object-to-directory

Import symbol

import * as object_to_directory from "@scroogieboy/object-to-directory";

Add Package

pnpm dlx jsr add @scroogieboy/object-to-directory

Import symbol

import * as object_to_directory from "@scroogieboy/object-to-directory";

Add Package

bunx jsr add @scroogieboy/object-to-directory

Import symbol

import * as object_to_directory from "@scroogieboy/object-to-directory";