本文へ移動
cccskills
無料GitHub で公開

derivation-flatten-array

Create an Estuary derivation that flattens a nested array into one output document per array element. Use for normalising nested JSON, exploding line items, unpacking tags, or producing one-row-per-item tables from arrays. Derivations add complexity and cost — confirm the user wants a derivation before reaching for this (some destinations can `UNNEST` server-side without one). Use when user says "flatten-array derivation", "derivation to explode an array", "transformation to unnest", "derive one row per item", or "flatten nested data with a derivation".

インストール方法を見る

含まれるファイル(1)

  • SKILL.md10.9 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

derivation-flatten-array

Stateless Estuary derivation that emits many output documents from a single source document by iterating over a nested array.

Prereq: read derivation-basics first for concepts, project layout, workflow, and language choice.

Docs: https://docs.estuary.dev/guides/flatten-array/ — the official TypeScript walkthrough for this exact pattern. Read it alongside this skill.

When to use this over alternatives

  • Normalising nested JSON: {order_id, line_items: [...]} → one doc per line item
  • Exploding tags/categories: {article_id, tags: ["a", "b", "c"]} → one doc per tag
  • Unpacking array columns from source databases for analytics
  • Preparing data for systems that prefer normalised 1-row-per-item tables

If the array isn't in the source document itself (e.g., you need to join across collections to assemble it), use derivation-join-collections. If you just need to keep the array intact but change how it's materialised (e.g., MongoDB flow_document), you don't need a derivation at all.

Canonical Example — TypeScript (recommended)

Orders arrive with an embedded array of line items. We emit one document per line item, carrying order context (customer, date) onto each row.

Project layout

my-derivation/
├── flow.yaml
├── schema.yaml
└── flatten.ts

schema.yaml

The output schema describes one line item — not the array:

type: object
properties:
  order_id:
    type: string
  line_id:
    type: string
  customer_id:
    type: string
  order_date:
    type: string
    format: date-time
  product_id:
    type: string
  product_name:
    type: string
  quantity:
    type: integer
  unit_price:
    type: number
  line_total:
    type: number
required: [order_id, line_id, product_id]

flow.yaml

collections:
  acmeCo/normalized/order-line-items:
    writeSchema: schema.yaml
    readSchema:
      allOf:
        - $ref: flow://write-schema
        - $ref: flow://inferred-schema
    key: [/order_id, /line_id]
    derive:
      using:
        typescript:
          module: flatten.ts
      transforms:
        - name: flattenLineItems
          source: acmeCo/production/orders
          shuffle: any

Key fields: the collection key is composite (order_id + line_id) — the array element id keeps output rows unique. shuffle: any because flattening is stateless. The writeSchema declares what the lambda emits; readSchema lets the inferred schema widen automatically as the lambda evolves. See derivation-basics § Schema setup.

flatten.ts

Generate the stub with flowctl generate --source flow.yaml, then fill in:

import { IDerivation, Document, SourceFlattenLineItems } from 'flow/acmeCo/normalized/order-line-items.ts';

export class Derivation extends IDerivation {
  flattenLineItems(_read: { doc: SourceFlattenLineItems }): Document[] {
    const order = _read.doc;

    // Missing or empty array → emit nothing
    if (!order.line_items || order.line_items.length === 0) {
      return [];
    }

    return order.line_items.map(item => ({
      order_id: order.order_id,
      line_id: item.line_id,
      customer_id: order.customer_id,
      order_date: order.order_date,
      product_id: item.product_id,
      product_name: item.product_name,
      quantity: item.quantity,
      unit_price: item.unit_price,
      line_total: item.quantity * item.unit_price,
    }));
  }
}

Return [] to drop the document entirely; return one document per array element to flatten.

Canonical Example — SQL

SQLite can flatten via json_each, which produces one row per array element. Works well for shallow arrays of primitives or simple objects; gets verbose for deeply nested structures (TypeScript is usually clearer then).

Tags array (simple case)

collections:
  acmeCo/normalized/article-tags:
    schema:
      type: object
      properties:
        article_id: { type: string }
        tag: { type: string }
      required: [article_id, tag]
    key: [/article_id, /tag]
    derive:
      using:
        sqlite: {}
      transforms:
        - name: flattenTags
          source: acmeCo/articles
          shuffle: any
          lambda: |
            SELECT
              $article_id AS article_id,
              json_each.value AS tag
            FROM json_each($tags);

Object array (more verbose)

lambda: |
  SELECT
    $order_id AS order_id,
    json_extract(json_each.value, '$.line_id')      AS line_id,
    json_extract(json_each.value, '$.product_id')   AS product_id,
    json_extract(json_each.value, '$.product_name') AS product_name,
    CAST(json_extract(json_each.value, '$.quantity') AS INTEGER)   AS quantity,
    json_extract(json_each.value, '$.unit_price')   AS unit_price
  FROM json_each($line_items);

For anything beyond simple fields, prefer the TypeScript form.

Test

Local validation — flowctl preview --fixture (works today)

fixture.jsonl:

["acmeCo/production/orders", {"order_id": "o1", "customer_id": "c1", "order_date": "2024-06-01T10:00:00Z", "line_items": [{"line_id": "li_1", "product_id": "p_a", "product_name": "Widget", "quantity": 2, "unit_price": 10.00}, {"line_id": "li_2", "product_id": "p_b", "product_name": "Gadget", "quantity": 1, "unit_price": 25.00}, {"line_id": "li_3", "product_id": "p_c", "product_name": "Gizmo", "quantity": 3, "unit_price": 5.50}]}]
["acmeCo/production/orders", {"order_id": "o2", "customer_id": "c2", "order_date": "2024-06-02T10:00:00Z", "line_items": []}]
{"commit": true}
flowctl preview --source flow.yaml \
  --name acmeCo/normalized/order-line-items \
  --fixture fixture.jsonl --timeout 30s

Expected output (3 docs from o1, nothing from o2's empty array):

{"order_id":"o1","line_id":"li_1","customer_id":"c1","order_date":"2024-06-01T10:00:00Z","product_id":"p_a","product_name":"Widget","quantity":2,"unit_price":10.00,"line_total":20.00}
{"order_id":"o1","line_id":"li_2","customer_id":"c1","order_date":"2024-06-01T10:00:00Z","product_id":"p_b","product_name":"Gadget","quantity":1,"unit_price":25.00,"line_total":25.00}
{"order_id":"o1","line_id":"li_3","customer_id":"c1","order_date":"2024-06-01T10:00:00Z","product_id":"p_c","product_name":"Gizmo","quantity":3,"unit_price":16.50}

Spec-defined tests — tests: block

tests:
  acmeCo/tests/flatten-line-items:
    - ingest:
        collection: acmeCo/production/orders
        documents:
          - order_id: "o1"
            customer_id: "c1"
            order_date: "2024-06-01T10:00:00Z"
            line_items:
              - { line_id: "li_1", product_id: "p_a", product_name: "Widget", quantity: 2, unit_price: 10.00 }
              - { line_id: "li_2", product_id: "p_b", product_name: "Gadget", quantity: 1, unit_price: 25.00 }
              - { line_id: "li_3", product_id: "p_c", product_name: "Gizmo",  quantity: 3, unit_price: 5.50 }
          - order_id: "o2"
            customer_id: "c2"
            order_date: "2024-06-02T10:00:00Z"
            line_items: []
    - verify:
        collection: acmeCo/normalized/order-line-items
        documents:
          - { order_id: "o1", line_id: "li_1", customer_id: "c1", order_date: "2024-06-01T10:00:00Z", product_id: "p_a", product_name: "Widget", quantity: 2, unit_price: 10.00, line_total: 20.00 }
          - { order_id: "o1", line_id: "li_2", customer_id: "c1", order_date: "2024-06-01T10:00:00Z", product_id: "p_b", product_name: "Gadget", quantity: 1, unit_price: 25.00, line_total: 25.00 }
          - { order_id: "o1", line_id: "li_3", customer_id: "c1", order_date: "2024-06-01T10:00:00Z", product_id: "p_c", product_name: "Gizmo",  quantity: 3, unit_price: 5.50,  line_total: 16.50 }

Variations

  • Simple string array (tags, prefs): json_each($tags) in SQL, _read.doc.tags.map(tag => ({...})) in TS.
  • JSON-string-encoded array: wrap JSON.parse in try/catch for TS; json_each(json($field)) in SQL where $field is a string.
  • Deeply nested arrays (categories→products): nested for...of loops in TS. SQL gets painful fast — prefer TS.
  • Conditional flattening: filter array elements before emitting (_read.doc.addresses.filter(a => a.is_primary).map(...)).
  • Include array index as a field: TS has .map((item, i) => ...); SQL uses json_each.key as the index.
  • Partial re-emit on upstream update: if the source document's array shrinks on update, stale output documents remain. See the gotcha below.

Gotchas specific to flatten-array

  • Key uniqueness is on you. The derived collection key must uniquely identify a flattened row. If the array elements have stable IDs (line_id), use them. If they don't, include the array index (item_index) in the key — otherwise multiple elements collide and reduce into one.
  • Source updates don't remove stale flattened rows. If an upstream order initially had 3 line items and the array later shrinks to 2, the derivation re-runs on the source update and emits 2 new rows — but the 3rd earlier row stays in the derived collection. If you need strict 1:1 sync with the source array, model deletions explicitly (e.g., source emits full array each update and you compare; or include a deleted flag per line item).
  • Empty/missing array is not an error. return [] in TS or FROM json_each(NULL) in SQL both produce zero output docs, which is the right behaviour — but surprises people expecting a per-source-doc output. The source document itself is not reflected in the derived collection.
  • json_each behavior on non-array input. Only json_each(NULL) and json_each('[]') silently yield zero rows. A JSON object yields one row per member, a scalar (json_each(42)) yields a single row, and a non-JSON string raises a malformed JSON error that surfaces as a transform failure. If you hit "no output", inspect the source with flowctl preview --source flow.yaml --name <source-collection> to see what the field actually contains.

Related

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Create an Alpaca capture using flowctl to stream stock trade data into Estuary collections. Use when setting up an Alpaca Market Data source for historical and real-time stock trades. Use when user says "capture Alpaca", "stream stock trades", "Alpaca market data", or "connect Alpaca to Estuary".

日本語の概要は準備中です。原文の説明を表示しています。

estuary/agent-skills72026年9月25日 更新

Create a capture for ANY Estuary connector using dynamic schema discovery. Use when the user wants to capture from a source that doesn't have a dedicated skill (e.g., Kafka, Salesforce, HubSpot, Stripe, S3, GCS, Kinesis, or any of the 148+ connectors). Use when user says "capture from <source>", "stream from <source>", "connect <source> to Estuary", or "set up <source> capture" and no connector-specific skill exists.

日本語の概要は準備中です。原文の説明を表示しています。

estuary/agent-skills72026年9月25日 更新

Create an HTTP Ingest (webhook) capture to receive data via HTTP POST requests. Use when setting up webhooks from GitHub, Shopify, Stripe, or any JSON source. Use when user says "set up webhook capture", "HTTP ingest to Estuary", "receive POST requests to Estuary", or "webhook capture".

日本語の概要は準備中です。原文の説明を表示しています。

estuary/agent-skills72026年9月25日 更新

Create a HubSpot Real-Time capture using flowctl to stream CRM data into Estuary collections. Use when setting up a HubSpot source for contacts, companies, deals, tickets, or other HubSpot resources. Use when user says "capture HubSpot", "stream from HubSpot", "HubSpot CDC", or "connect HubSpot to Estuary".

日本語の概要は準備中です。原文の説明を表示しています。

estuary/agent-skills72026年9月25日 更新

Create a MongoDB CDC capture using flowctl. Use when setting up real-time streaming from MongoDB Atlas, DocumentDB, or self-hosted MongoDB. Use when user says "capture MongoDB", "stream from Mongo", "MongoDB CDC", or "connect MongoDB to Estuary".

日本語の概要は準備中です。原文の説明を表示しています。

estuary/agent-skills72026年9月25日 更新

Create a MySQL CDC capture using flowctl with binlog replication. Use when setting up streaming from MySQL, Amazon RDS MySQL, or Aurora MySQL. Use when user says "capture MySQL", "stream from MySQL", "MySQL CDC", "binlog replication", or "connect MySQL to Estuary".

日本語の概要は準備中です。原文の説明を表示しています。

estuary/agent-skills72026年9月25日 更新

estuary のスキルをすべて見る

このスキルの問題を報告する