{"_id":"@efesto-cloud/mongodb-expand","name":"@efesto-cloud/mongodb-expand","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@efesto-cloud/mongodb-expand","version":"1.0.0","description":"MongoDB $lookup expand (eager-loading) for efesto-cloud","repository":{"type":"git","url":"git+https://github.com/efesto-cloud/lib.git","directory":"packages/mongodb-expand"},"type":"module","module":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"import":"./dist/index.js","types":"./dist/index.d.ts"},"./*":{"import":"./dist/*.js","types":"./dist/*.d.ts"}},"dependencies":{"@efesto-cloud/expand":"1.0.0"},"keywords":[],"license":"MIT","publishConfig":{"access":"public"},"devDependencies":{"mongodb":"^7"},"scripts":{"build":"tsc --project tsconfig.json","typecheck":"tsc --noEmit","clean":"rm -rf dist"},"_id":"@efesto-cloud/mongodb-expand@1.0.0","bugs":{"url":"https://github.com/efesto-cloud/lib/issues"},"homepage":"https://github.com/efesto-cloud/lib#readme","_integrity":"sha512-VNi6Bh9iOJ5U4aZdtVOAjkPTg2ZJ3nWlRM8rTNeQ6m3kqpgueWwfx9QJB1kJP7S9n0K0UsNTMsGJs57wjJLraA==","_resolved":"/tmp/06610a9f6fedf5cd9b4608f63fff2e22/efesto-cloud-mongodb-expand-1.0.0.tgz","_from":"file:efesto-cloud-mongodb-expand-1.0.0.tgz","_nodeVersion":"24.16.0","_npmVersion":"11.13.0","dist":{"integrity":"sha512-VNi6Bh9iOJ5U4aZdtVOAjkPTg2ZJ3nWlRM8rTNeQ6m3kqpgueWwfx9QJB1kJP7S9n0K0UsNTMsGJs57wjJLraA==","shasum":"d120c4c4a6f2796c02a13da1afc2410d8f2e3d77","tarball":"https://registry.npmjs.org/@efesto-cloud/mongodb-expand/-/mongodb-expand-1.0.0.tgz","fileCount":15,"unpackedSize":20635,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@efesto-cloud%2fmongodb-expand@1.0.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDJm+Bp1AdX/tQLoy/PTWKHjyU12IFpI8WDf44vsRrkNAIgDY8f8+8Q5mRno5MBuxWcldlJMmNFn1pOM0j6WL2qqVI="}]},"_npmUser":{"name":"dariofurlan","email":"dario@dariofurlan.com"},"directories":{},"maintainers":[{"name":"dariofurlan","email":"dario@dariofurlan.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/mongodb-expand_1.0.0_1780908804534_0.7015381355041832"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-08T08:53:24.404Z","1.0.0":"2026-06-08T08:53:24.667Z","modified":"2026-06-08T08:53:25.095Z"},"maintainers":[{"name":"dariofurlan","email":"dario@dariofurlan.com"}],"description":"MongoDB $lookup expand (eager-loading) for efesto-cloud","homepage":"https://github.com/efesto-cloud/lib#readme","keywords":[],"repository":{"type":"git","url":"git+https://github.com/efesto-cloud/lib.git","directory":"packages/mongodb-expand"},"bugs":{"url":"https://github.com/efesto-cloud/lib/issues"},"license":"MIT","readme":"# @efesto-cloud/mongodb-expand\n\nMongoDB runtime for the [`@efesto-cloud/expand`](../expand) spec types. Ships:\n\n- `BaseExpander` — builds a `$lookup`-based aggregation pipeline from a normalized expand spec.\n- `QueryBuilder` — composes `$match` / `$set` / `$sort` / `$skip` / `$limit` and appends an expand pipeline in the right order.\n\n## Installation\n\n```bash\npnpm add @efesto-cloud/mongodb-expand @efesto-cloud/expand mongodb\n```\n\n## How it fits together\n\nFor each entity you want to expand, you write three small files:\n\n1. **Shape** — which fields are expandable, and whether they're leaves or nested.\n2. **Expander** — extends `BaseExpander`, turns a spec into `$lookup` stages.\n3. **QueryBuilder** — extends `QueryBuilder`, exposes `expandWith(spec)` for repositories.\n\nThen the repository composes a pipeline with the builder and runs `aggregate()`.\n\n## `BaseExpander`\n\n```ts\nabstract class BaseExpander<TShape, TCollection extends string> {\n    protected lookup(options: {\n        from: TCollection;\n        localField: string;\n        foreignField: string;\n        as: string;\n        pipeline?: Document[];\n    }): Document;\n\n    protected unwind(path: string): Document;\n    protected addStages(...stages: Document[]): void;\n\n    protected markExpanded(field: string): boolean; // false if already expanded\n    protected isExpanded(field: string): boolean;\n\n    abstract expand(spec: NormalizedExpand<TShape>): this;\n\n    build(): Document[];\n}\n```\n\n### Example: flat expander\n\n```ts\nimport { BaseExpander } from \"@efesto-cloud/mongodb-expand\";\nimport type { NormalizedExpand } from \"@efesto-cloud/expand\";\n\ntype PostShape = {\n    author: true;    // 1:1, leaf\n    comments: true;  // 1:many, leaf\n};\n\nexport default class PostExpander extends BaseExpander<PostShape> {\n    static readonly SHAPE: PostShape = { author: true, comments: true };\n\n    private author() {\n        if (!this.markExpanded(\"author\")) return;\n        this.addStages(\n            this.lookup({\n                from: \"authors\",\n                localField: \"author_id\",\n                foreignField: \"_id\",\n                as: \"author\",\n            }),\n            this.unwind(\"author\"), // 1:1 — flatten array\n        );\n    }\n\n    private comments() {\n        if (!this.markExpanded(\"comments\")) return;\n        this.addStages(\n            this.lookup({\n                from: \"comments\",\n                localField: \"_id\",\n                foreignField: \"post_id\",\n                as: \"comments\",\n            }),\n            // No unwind — keep as array\n        );\n    }\n\n    expand(spec: NormalizedExpand<PostShape>): this {\n        if (spec.author) this.author();\n        if (spec.comments) this.comments();\n        return this;\n    }\n\n    static buildPipeline(spec: NormalizedExpand<PostShape>): Document[] {\n        return new PostExpander().expand(spec).build();\n    }\n}\n```\n\n### Nested expansion\n\nWhen a related entity itself has an expander, pass its pipeline as a sub-pipeline:\n\n```ts\nprivate author(nestedSpec: NormalizedExpand<AuthorShape>) {\n    if (!this.markExpanded(\"author\")) return;\n    this.addStages(\n        this.lookup({\n            from: \"authors\",\n            localField: \"author_id\",\n            foreignField: \"_id\",\n            as: \"author\",\n            pipeline: AuthorExpander.buildPipeline(nestedSpec),\n        }),\n        this.unwind(\"author\"),\n    );\n}\n```\n\n## `QueryBuilder`\n\n```ts\nclass QueryBuilder<D extends Document, C extends string = string> {\n    match(filter: Filter<D>): this;\n    set(doc: Document): this;\n    sort(spec: { [K in keyof D]?: 1 | -1 }): this;\n    skip(n: number): this;\n    limit(n: number): this;\n    page(page?: number, pageSize?: number): this; // convenience: skip + limit\n    build(): Document[];\n\n    // protected — use from subclass to expose a typed expandWith()\n    protected expand(lookup, { unwind }): this;\n    protected push_expand_pipeline(pipeline: Document[]): this;\n}\n```\n\nStage order in `build()`: `$match` → `$set` → `$sort` → `$skip` → `$limit` → expand pipeline. Filters and pagination run before the joins so `$lookup` only runs against the final result set.\n\n### Example: typed expand-aware builder\n\n```ts\nimport QueryBuilder from \"@efesto-cloud/mongodb-expand/QueryBuilder\";\nimport { normalizeExpand, type Expand } from \"@efesto-cloud/expand\";\nimport PostExpander from \"./PostExpander.js\";\nimport type { PostShape } from \"./PostShape.js\";\n\nexport default class PostQueryBuilder extends QueryBuilder<PostDocument> {\n    expandWith(fields: Expand<PostShape> = {}): this {\n        const normalized = normalizeExpand(fields, PostExpander.SHAPE);\n        const pipeline = PostExpander.buildPipeline(normalized);\n        this.push_expand_pipeline(pipeline);\n        return this;\n    }\n}\n```\n\n### Using it in a repository\n\n```ts\nasync get(id: ObjectId, options?: { expand?: Expand<PostShape> }) {\n    const pipeline = new PostQueryBuilder()\n        .match({ _id: id })\n        .expandWith(options?.expand)\n        .limit(1)\n        .build();\n\n    const docs = await this.coll\n        .aggregate<PostDocument>(pipeline, { session: this.uow.session })\n        .toArray();\n\n    return docs.length === 0 ? Maybe.none() : Maybe.maybe(PostMapper.from(docs[0]!));\n}\n```\n\n## Rules of thumb\n\n- **1:1 (FK on this entity)** — `lookup` + `unwind`.\n- **1:many (FK on the related entity)** — `lookup` only, no `unwind`.\n- **Array of FKs** — `lookup` with `localField` as the array field, no `unwind` (result is an array).\n- **Optional FK** — `preserveNullAndEmptyArrays: true` is the default behaviour of `unwind()` here; map `doc.foo ? FooMapper.from(doc.foo) : null`.\n\n## Related\n\n- [`@efesto-cloud/expand`](../expand) — spec types and `normalizeExpand`.\n","readmeFilename":"README.md","_rev":"1-6248db607f16fedc67344abbd62feebc"}