# @x12i/helpers

> Small helper utilities for x12i projects. Cloud/DB backends (Firebase, GCS, S3, Mongo) are optional peer dependencies via subpath imports.

Latest version **3.0.0** (published 2026-08-05) · MIT license · 0 weekly downloads

## Install

```sh
npm install @x12i/helpers
pnpm add @x12i/helpers
yarn add @x12i/helpers
bun add @x12i/helpers
```

## Health

**Score 60/100 (C)** — status: active.

Positive: esm support; no vulnerabilities; recently updated; high maintenance score.

Warnings: low downloads; no types.

## Facts

| | |
|---|---|
| Version | 3.0.0 |
| Published | 2026-08-05 |
| First published | 2026-04-14 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | none |
| Module format | ESM + CommonJS |
| Dependencies | 1 |
| Unpacked size | 274.7 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Author | x12i |
| Maintainers | x12i |
| Keywords | helpers, utilities, mapper, json, object-mapping, enrichment, data-join |

## Links

- npm: https://www.npmjs.com/package/@x12i/helpers
- Repository: https://github.com/x12i/helpers
- Homepage: https://www.npmjs.com/package/@x12i/helpers
- Issues: https://github.com/x12i/helpers/issues
- npm.io page: https://npm.io/package/@x12i/helpers

## Dependencies (1)

- [@x12i/env](https://npm.io/package/@x12i/env.md) ^4.0.1

## Alternatives

- [@mapbox/jsonlint-lines-primitives](https://npm.io/package/@mapbox/jsonlint-lines-primitives.md) — 5.3M weekly downloads
- [reftools](https://npm.io/package/reftools.md) — 3.5M weekly downloads
- [@hey-api/openapi-ts](https://npm.io/package/@hey-api/openapi-ts.md) — 3.5M weekly downloads
- [@mapbox/geojson-rewind](https://npm.io/package/@mapbox/geojson-rewind.md) — 2.4M weekly downloads
- [turbo-stream](https://npm.io/package/turbo-stream.md) — 1.7M weekly downloads

## Recent versions

- 3.0.0 (latest) — 2026-08-05
- 2.1.0 — 2026-07-02
- 2.0.0 — 2026-06-30
- 1.8.0 — 2026-06-30
- 1.7.0 — 2026-04-29
- 1.5.2 — 2026-04-20
- 1.5.1 — 2026-04-20
- 1.5.0 — 2026-04-19
- 1.4.0 — 2026-04-18
- 1.2.4 — 2026-04-15
- 1.2.3 — 2026-04-15
- 1.2.1 — 2026-04-15
- 1.2.0 — 2026-04-15
- 1.1.0 — 2026-04-14
- 1.0.2 — 2026-04-14
- … 2 more at https://npm.io/package/@x12i/helpers/versions

## README

# @x12i/helpers

Small helper utilities for x12i projects.

## Install

```bash
npm i @x12i/helpers
```

**v3 breaking change:** Firebase, GCS, S3, and MongoDB SDKs are **optional peer dependencies**. They are not installed with the base package. Import those features via **subpaths** and install the matching peer:

```bash
# Google Cloud Storage
npm i @x12i/helpers @google-cloud/storage

# Firebase RTDB / Firestore
npm i @x12i/helpers firebase-admin

# Amazon S3
npm i @x12i/helpers @aws-sdk/client-s3 @aws-sdk/lib-storage

# MongoDB
npm i @x12i/helpers mongodb
```

The package root (`require("@x12i/helpers")`) exports pure helpers only (mapper, HTTP tools, contracts, record-transform, enrichment). Cloud/DB clients are subpath-only.

## Helpers (library of abilities)

Each helper has a focused doc page under `helpers-docs/`:

- **Objects mapper**: `helpers-docs/objects-mapper.md`
- **Record transform (protocol v2.0)**: `helpers-docs/record-transform.md` (v1 acceptance: `helpers-docs/v1-acceptance-checklist.md`)
- **Record key analysis (identifier discovery)**: `helpers-docs/record-key-analysis.md`
- **Link-based enrichment**: `helpers-docs/enrichment.md`
- **HTTP tools**: `helpers-docs/http-tools.md`
- **API contracts**: `helpers-docs/api-contracts.md`
- **API mapper / protocol bridge**: `helpers-docs/api-mapper.md`
- **Firebase RTDB (mongo-like)**: `helpers-docs/firebase-rtdb.md`
- **Firebase Firestore (admin init + databaseId)**: `helpers-docs/firebase-firestore.md`
- **Google Cloud Storage (object I/O)**: `helpers-docs/gcs.md`
- **MongoDB (mongo-like collection API)**: `helpers-docs/mongo.md`
- **Amazon S3 (object I/O, GCS-shaped API)**: `helpers-docs/s3.md`

## Usage

### Objects mapper

```js
const { mapObject, createMapper, mapArray, deepGet, deepSet } = require("@x12i/helpers");

const source = { user: { first: "Jane", last: "Doe" } };

const mapping = [
  "user.first -> profile.firstName",
  "user.last -> profile.lastName",
  { template: "{user.first} {user.last}", to: "profile.displayName" }
];

console.log(mapObject(source, mapping));
// { profile: { firstName: 'Jane', lastName: 'Doe', displayName: 'Jane Doe' } }
```

You can also import the mapper directly:

```js
const { mapObject } = require("@x12i/helpers/objects-mapper");
```

### Record transform (protocol v2.0)

Declarative JSON record transformer — apply a versioned spec to reshape, validate, and enrich a single object.

**Docs:** `helpers-docs/record-transform.md` · **Acceptance checklist:** `helpers-docs/v1-acceptance-checklist.md`

```js
const { transform, validateSpec, TransformError } = require("@x12i/helpers/record-transform");
// also available from the package root: require("@x12i/helpers")

const spec = {
  version: "1.0",
  operations: [
    { op: "move", from: "details.sev", to: "metadata.severity" },
    {
      op: "mapValue",
      path: "metadata.severity",
      mapping: { 0: "none", 1: "low", 2: "medium", 3: "high", 4: "critical" },
      default: "unknown",
    },
    {
      op: "rule",
      when: {
        type: "number",
        path: "metadata.priority",
        operator: "gte",
        compareTo: { value: 8 },
      },
      then: { action: "set", path: "metadata.escalate", value: true },
    },
  ],
};

const check = validateSpec(spec);
if (!check.valid) throw new Error(check.errors.join("; "));

const { record, valid, violations } = transform(
  { details: { sev: 3 }, metadata: { priority: 9 } },
  spec
);
// record.metadata.severity === "high"
// record.metadata.escalate === true
```

**API**

| Export | Description |
|--------|-------------|
| `transform(record, spec)` | Returns `{ record, valid, violations }`. Input is never mutated. |
| `validateSpec(spec)` | Pre-flight structural validation → `{ valid, errors }`. |
| `TransformError` | Thrown for invalid specs, `onMissing: "error"`, `strict` mapValue, `cast onError: "error"`, etc. |

**Version policy:** only `"1.0"` is supported. `validateSpec` rejects other versions; `transform` throws `TransformError` before execution.

**Operations:** `move`, `rename`, `mapValue`, `cast`, `drop`, `rule`, `validate`. Rule actions (v1): `set` with `value` or `valueFrom`.

**Tests:** `npm test -- test/recordTransform.acceptance.test.js`

### Record key analysis (identifier discovery)

Three-tier heuristics for finding identifier-like fields and cross-collection relationships. Tier 1 scoring is **JSON-driven** (keywords, regex, anti-keywords) via `defaultHeuristics.json`.

**Docs:** `helpers-docs/record-key-analysis.md`

```js
const {
  analyzeSingleRecordKeys,
  analyzeCollectionKeys,
  mapCrossCollectionRelationships,
} = require("@x12i/helpers/record-key-analysis");

const candidates = analyzeSingleRecordKeys({
  _id: "507f1f77bcf86cd799439011",
  user_id: "a1b2c3d4-e5f6-4789-a012-3456789abcde",
  name: "Jane",
});

const classified = analyzeCollectionKeys(rows, candidates.map((c) => c.property));

const joins = mapCrossCollectionRelationships(orders, "user_id", {
  users: { primaryKeys: ["id"], records: users },
});
```

**Tests:** `npm test -- test/recordKeyAnalysis.test.js`

### Link-based enrichment

Generic join helper for linking enrichment collections onto target records — by IP, normalized username, id, etc. Two layers: `enrichLink` (core matcher) and `enrichFromMetadata` (metadata-driven rule runner).

**Docs:** `helpers-docs/enrichment.md`

```js
const { enrichLink, enrichFromMetadata, applyPatch } = require("@x12i/helpers/enrichment");

const linkedUsers = enrichLink({
  enrichmentCollection: usersArray,
  enrichmentLinkKey: "ip",
  target: someAsset,
  targetLinkKey: "data.assetIp",
  matchMode: "exact",
  fieldMap: [
    { from: "user", to: "user" },
    { from: "vsys", to: "vsys" },
  ],
});

const patch = enrichFromMetadata(someAsset, { objectType: "assets", dataDir: "./data" });
const merged = applyPatch(someAsset, patch);
```

**Tests:** `npm test -- test/enrichment.test.js`

### HTTP tools (request ⇄ curl, base URLs)

```js
const {
  requestToCurl,
  curlToRequest,
  buildUrl,
  createBaseUrlClient,
} = require("@x12i/helpers");

const curl = requestToCurl({
  method: "POST",
  url: "https://api.example.com/v1/users",
  headers: { authorization: "Bearer TOKEN" },
  body: { name: "Jane" },
});

// curl -X POST -H 'authorization: Bearer TOKEN' -H 'content-type: application/json' --data-raw '{"name":"Jane"}' 'https://api.example.com/v1/users'
console.log(curl);

const req = curlToRequest(curl);
// { method: 'POST', url: 'https://api.example.com/v1/users', headers: { authorization: 'Bearer TOKEN', 'content-type': 'application/json' }, body: '{"name":"Jane"}' }
console.log(req);

console.log(buildUrl("https://api.example.com/", "/v1/users", { limit: 10, tags: ["a", "b"] }));
// https://api.example.com/v1/users?limit=10&tags=a&tags=b

const api = createBaseUrlClient("https://api.example.com/", {
  headers: { authorization: "Bearer TOKEN" },
});
console.log(api.curl("/v1/health"));
```

Direct import:

```js
const { requestToCurl } = require("@x12i/helpers/http-tools");
```

### API contracts + protocol bridge

Define protocol-aware API contracts (REST/GraphQL/JSON-RPC/SOAP/custom) with optional request/response validation and auth application.

```js
const { defineContract, createRegistry } = require("@x12i/helpers/api-contracts");
const { createBridge } = require("@x12i/helpers/api-mapper");

const registry = createRegistry();

registry.register({
  name: "restGetUser",
  protocol: "rest",
  endpoint: "/api/users/{id}",
  method: "GET",
  auth: { type: "bearer" },
});

registry.register({
  name: "gqlGetUser",
  protocol: "graphql",
  endpoint: "https://api.example.com/graphql",
  query: "query GetUser($userId: ID!) { user(id: $userId) { id fullName } }",
  auth: { type: "bearer" },
});

const bridge = createBridge({
  source: "restGetUser",
  target: "gqlGetUser",
  registry,
  requestMapping: [{ from: "pathParams.id", to: "variables.userId" }],
  responseMapping: ["user.id", "user.fullName -> user.name"],
  credentials: { target: { token: "TOKEN" } },
});

// bridge.call({ pathParams: { id: "42" } })
```

Direct imports:

```js
const { createBridge, batchCall, chainBridges } = require("@x12i/helpers/api-mapper");
const { defineContract, createRegistry } = require("@x12i/helpers/api-contracts");
```

### Firebase Realtime Database (native) — mongo-like helper

This helper uses the Firebase Admin SDK and wraps RTDB with a familiar Mongo-ish API (`findOne`, `insertOne`, `updateOne`, etc.).

This is **not** a Firestore helper. If your upstream app uses **Firestore**, you should initialize `firebase-admin` with service account credentials and use `admin.firestore()` (Firestore does **not** use `FIREBASE_DATABASE_URL`).

1) Create `.env` (see `.env.example`) and provide:

- `GOOGLE_SERVICE_ACCOUNT_BASE64`: Base64-encoded Google service account JSON
- `FIREBASE_DATABASE_URL`: the **exact** Realtime Database URL for that same Firebase project (copied from Firebase Console → Realtime Database → Data)

#### Credentials (Base64 only)

All helpers use a Base64-encoded Google service account JSON.

Why:
- Works in serverless / edge environments
- No filesystem dependency
- Safer secret handling

How to generate:

```bash
cat service-account.json | base64
```

Then set:

```env
GOOGLE_SERVICE_ACCOUNT_BASE64=...
```

#### Migration

Before:

```env
FIREBASE_SERVICE_ACCOUNT_PATH=.secrets/firebase-service-account.json
```

After:

```bash
cat .secrets/firebase-service-account.json | base64
```

```env
GOOGLE_SERVICE_ACCOUNT_BASE64=<result>
```

2) Use it:

```js
const { initFirebaseRtdb } = require("@x12i/helpers/firebase-rtdb");

const fb = initFirebaseRtdb(); // loads .env by default
const users = fb.collection("users");

const { insertedId } = await users.insertOne({ email: "a@b.com", name: "Ami" });
const user = await users.findOne({ _id: insertedId });

await users.updateOne({ _id: insertedId }, { $set: { name: "Ami N." }, $inc: { loginCount: 1 } });
await users.deleteOne({ _id: insertedId });
```

Important:

- **Service account + RTDB URL must match the same Firebase project.**
- **Do not guess** the RTDB URL (`firebaseio.com` vs `firebasedatabase.app`, region, etc.).
  In Firebase Console, open the project referenced by your service account JSON (`project_id`), then copy the **Database URL** shown under **Realtime Database → Data** and set `FIREBASE_DATABASE_URL` to that exact value.

Optional: to fail fast (avoid long hangs in test suites) set `FIREBASE_CONNECT_TIMEOUT_MS` or pass `connectTimeoutMs`:

```js
const fb = initFirebaseRtdb({ connectTimeoutMs: 5000 });
```

If you want native RTDB query features (server-side indexed filtering), use `query()`:

```js
const result = await users
  .query({ orderByChild: "email", equalTo: "a@b.com", limitToFirst: 10 })
  .find();
```

### Firebase Firestore — admin helper

If you want **Firestore** (not RTDB), use this helper. Firestore does **not** require a database URL; it authenticates via service account credentials.

1) Set `GOOGLE_SERVICE_ACCOUNT_BASE64` (Base64-encoded service account JSON).

2) Ensure Firestore exists for that project:

- Firebase Console → **Firestore Database** → **Create database** (this creates the default database for the project)

2) Use it:

```js
const { initFirebaseFirestore } = require("@x12i/helpers/firebase-firestore");

const { firestore } = initFirebaseFirestore(); // loads .env by default
const docRef = firestore.doc("catalogs/appId");

await docRef.set({ hello: "world" }, { merge: true });
const snap = await docRef.get();
console.log(snap.exists, snap.data());
```

Optional: fail fast (avoid long hangs in test suites) set `FIRESTORE_CONNECT_TIMEOUT_MS` (or `FIREBASE_CONNECT_TIMEOUT_MS`) or pass `connectTimeoutMs`:

```js
const fb = initFirebaseFirestore({ connectTimeoutMs: 5000 });
```

Defaults:

- `FIRESTORE_DATABASE_ID` defaults to `catalox`
- `FIREBASE_PROJECT_ID` defaults to `x12i` (normally inferred from the service account’s `project_id`, but you can override it)

If you want a different Firestore database id, set `FIRESTORE_DATABASE_ID` or pass `databaseId`:

```js
initFirebaseFirestore({ databaseId: "catalox" });
```

### Google Cloud Storage (GCS)

Server-side object storage only: import the **subpath** `@x12i/helpers/gcs` so the main package entry does not load `@google-cloud/storage`.

1) Set `GOOGLE_SERVICE_ACCOUNT_BASE64` (Base64-encoded service account JSON).
2) Set a bucket name (defaults to `storage_bucket`; `STORAGE_BUCKET` / `GCS_BUCKET` also supported) or pass `{ bucket }`.

2) Minimal usage:

```js
const { createGcsClient } = require("@x12i/helpers/gcs");

const gcs = createGcsClient({
  // envPath defaults to ".env"; loads GCS_BUCKET / GCS_OBJECT_PREFIX if set
  prefix: "my-app/uploads",
});

await gcs.uploadObject("user/1/avatar.png", buffer, { contentType: "image/png" });
const data = await gcs.readObjectBuffer("user/1/avatar.png", { maxBytes: 2 * 1024 * 1024 });
const { items, nextPageToken } = await gcs.listObjects({ prefix: "user/1/", maxResults: 50 });
```

When to use this helper instead of `@google-cloud/storage` directly:

- You want **shared x12i conventions** for loading `.env` and service account material (same Base64-only credential mechanism as RTDB/Firestore helpers).
- You want **small, stable helpers** for upload, buffered read (with a **max size**), list pagination, existence checks, and metadata, plus **typed errors** (`GcsNotFoundError`, `GcsPermissionDeniedError`, `GcsFailedPreconditionError`) for mapping to your own issue types.

Optional **emulator** (CI or local): set `STORAGE_EMULATOR_HOST` (for example with [fake-gcs-server](https://github.com/fsouza/fake-gcs-server)). See `test/gcs.emulator.test.js` for an example command line.

**Live tests** in this repo: set `STORAGE_LIVE_TESTS=1` (or legacy `GCS_LIVE_TESTS=1`) and `STORAGE_BUCKET` (or `GCS_BUCKET`) to a dedicated test bucket; see `test/gcs.live.test.js`.

### MongoDB (mongo-like, RTDB-shaped API)

Subpath: `@x12i/helpers/mongo`. Exposes **`collection`**, **`ref`**, **`db`**, and **`client`** with the same mongo-like surface as `@x12i/helpers/firebase-rtdb` (`insertOne`, `find`, `findOne`, `updateOne`, `query`, per-doc `get`/`set`/`update`/`delete`, etc.). Uses **`MONGO_URI`** (or `MONGODB_URI`) from `.env` after loading via the same env loader as other helpers.

`initMongoDb` is **async** (MongoDB must connect over the network) unlike synchronous Firebase RTDB init:

```js
const { initMongoDb, getMongoDb } = require("@x12i/helpers/mongo");

const mongo = await initMongoDb(); // reads MONGO_URI from .env
const users = mongo.collection("app/users");
const { insertedId } = await users.insertOne({ email: "a@b.com" });
await mongo.ref("app").remove(); // drops collections under path prefix (see docs)
```

Live tests: `MONGO_LIVE_TESTS=1` — see `test/mongo.live.test.js`.

### Amazon S3 (object I/O)

Subpath: `@x12i/helpers/s3`. Same method names as GCS: **`createS3Client`**, **`uploadObject`**, **`openReadStream`**, **`readObjectBuffer`**, **`listObjects`**, **`objectExists`**, **`deleteObject`**, **`getObjectMetadata`**, **`setObjectMetadata`**, plus matching error classes (`S3NotFoundError`, …). Configure **`S3_BUCKET`**, **`AWS_REGION`**, optional **`S3_ENDPOINT`** / **`S3_FORCE_PATH_STYLE`** for MinIO or LocalStack, and standard AWS credentials.

```js
const { createS3Client } = require("@x12i/helpers/s3");

const s3 = createS3Client({ prefix: "my-app/uploads" });
await s3.uploadObject("k.txt", "hi", { contentType: "text/plain" });
```

Live tests: `S3_LIVE_TESTS=1` — see `test/s3.live.test.js`.

## Helper documentation

See `helpers-docs/` for per-helper docs (what it is, why it exists, usage, and any `.env` keys it reads).

## API

- `mapObject(source, mapping, opts)`
- `createMapper(mapping, opts)`
- `mapArray(sourceArray, mapping, opts)`
- `deepGet(obj, path)`
- `deepSet(obj, path, value)`
- `requestToCurl(req)`
- `curlToRequest(curlCommand)`
- `buildUrl(baseUrl, pathOrUrl, query)`
- `createBaseUrlClient(baseUrl, defaults)`
- `defineContract(config)`
- `createRegistry()`
- `createBridge(config)`
- `batchCall(bridge, paramsList, opts)`
- `chainBridges(...bridges)`
- `initFirebaseRtdb(options)`
- `getFirebaseRtdb()`
- `initFirebaseFirestore(options)`
- `getFirebaseFirestore()`
- `enrichLink(options)`
- `defaultNormalize(value)`
- `enrichFromMetadata(target, options?)`
- `applyPatch(target, patch)`
- `getByPath(obj, path)` / `setByPath(obj, path, value)` / `stripArrayMarker(path)`

Subpath only (not re-exported from the root package):

- `@x12i/helpers/gcs`: `createGcsClient(options)`, `GcsHelperError`, `GcsNotFoundError`, `GcsPermissionDeniedError`, `GcsFailedPreconditionError`, `GcsObjectTooLargeError`
- `@x12i/helpers/mongo`: `initMongoDb(options)`, `getMongoDb()`
- `@x12i/helpers/s3`: `createS3Client(options)`, `S3HelperError`, `S3NotFoundError`, `S3PermissionDeniedError`, `S3FailedPreconditionError`, `S3ObjectTooLargeError`

---
_Source: https://npm.io/package/@x12i/helpers · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
