@postrun/js 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-A545VJ5X.js → chunk-H2IIX5BP.js} +17 -4
- package/dist/chunk-H2IIX5BP.js.map +1 -0
- package/dist/{chunk-IMU3SG45.js → chunk-VGR3BQCT.js} +339 -13
- package/dist/chunk-VGR3BQCT.js.map +1 -0
- package/dist/index.cjs +394 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +52 -8
- package/dist/index.d.ts +52 -8
- package/dist/index.js +46 -11
- package/dist/index.js.map +1 -1
- package/dist/schemas/index.cjs +338 -10
- package/dist/schemas/index.cjs.map +1 -1
- package/dist/schemas/index.d.cts +345 -14
- package/dist/schemas/index.d.ts +345 -14
- package/dist/schemas/index.js +1 -1
- package/dist/server.cjs +336 -10
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +2 -2
- package/dist/{types.gen-DGCs7eB8.d.cts → types.gen-lbiXwxGS.d.cts} +793 -82
- package/dist/{types.gen-DGCs7eB8.d.ts → types.gen-lbiXwxGS.d.ts} +793 -82
- package/package.json +1 -1
- package/dist/chunk-A545VJ5X.js.map +0 -1
- package/dist/chunk-IMU3SG45.js.map +0 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/compose.ts"],"names":[],"mappings":";;;;AAuEO,IAAM,YAAA,GAAN,cAA2B,KAAA,CAAM;AAAA,EACtC,YAAY,OAAA,EAAiB;AAC3B,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,cAAA;AAAA,EACd;AACF;AA4BA,SAAS,WAAW,KAAA,EAA8B;AAChD,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,IAAI,SAAA,GAAY,CAAA;AAChB,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,IAAI,IAAA,CAAK,IAAA,KAAS,OAAA,EAAS,MAAA,IAAU,CAAA;AAAA,SAAA,IAC5B,IAAA,CAAK,IAAA,KAAS,UAAA,EAAY,SAAA,IAAa,CAAA;AAAA,SAC3C,MAAA,IAAU,CAAA;AAAA,EACjB;AACA,EAAA,OAAO,EAAE,MAAA,EAAQ,MAAA,EAAQ,SAAA,EAAU;AACrC;AAEA,SAAS,eAAA,CAAgB,OAA8B,QAAA,EAAwB;AAC7E,EAAA,IAAI,UAAA,CAAW,KAAK,CAAA,CAAE,SAAA,GAAY,CAAA,EAAG;AACnC,IAAA,MAAM,IAAI,YAAA,CAAa,CAAA,EAAG,QAAQ,CAAA,mCAAA,CAAqC,CAAA;AAAA,EACzE;AACF;AAOA,SAAS,qBAAA,CACP,KAAA,EACA,QAAA,EACA,SAAA,EACoC;AACpC,EAAA,MAAM,EAAE,MAAA,EAAQ,MAAA,EAAO,GAAI,WAAW,KAAK,CAAA;AAC3C,EAAA,IAAI,MAAA,GAAS,CAAA,IAAK,MAAA,GAAS,CAAA,EAAG;AAC5B,IAAA,MAAM,IAAI,YAAA;AAAA,MACR,GAAG,QAAQ,CAAA,kFAAA;AAAA,KACb;AAAA,EACF;AACA,EAAA,IAAI,SAAS,CAAA,EAAG;AACd,IAAA,MAAM,IAAI,YAAA,CAAa,CAAA,EAAG,QAAQ,CAAA,mCAAA,CAAqC,CAAA;AAAA,EACzE;AACA,EAAA,IAAI,MAAA,KAAW,GAAG,OAAO,SAAA;AACzB,EAAA,OAAO,MAAA,IAAU,IAAI,aAAA,GAAgB,cAAA;AACvC;AAEA,IAAM,QAAA,GAAiC;AAAA,EACrC,cAAA,EAAgB,CAAC,KAAA,KAAU;AACzB,IAAA,IAAI,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG,OAAO,MAAA;AAC/B,IAAA,eAAA,CAAgB,OAAO,GAAG,CAAA;AAC1B,IAAA,OAAO,qBAAA,CAAsB,KAAA,EAAO,GAAA,EAAK,OAAO,CAAA;AAAA,EAClD,CAAA;AAAA,EACA,YAAA,EAAc,CAAC,EAAE,QAAA,EAAU,UAAU,YAAA,EAAc,IAAA,EAAM,OAAM,MAAO;AAAA,IACpE,QAAA,EAAU,GAAA;AAAA,IACV,SAAA,EAAW,QAAA;AAAA,IACX,aAAA,EAAe,YAAA;AAAA,IACf,IAAA;AAAA,IACA,KAAA,EAAO,CAAC,GAAG,KAAK,CAAA;AAAA,IAChB;AAAA,GACF;AACF,CAAA;AAEA,IAAM,eAAA,GAA+C;AAAA,EACnD,cAAA,EAAgB,CAAC,KAAA,KAAU;AACzB,IAAA,IAAI,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG,OAAO,MAAA;AAC/B,IAAA,IAAI,UAAA,CAAW,KAAK,CAAA,CAAE,SAAA,GAAY,CAAA,EAAG;AAGnC,MAAA,IAAI,KAAA,CAAM,SAAS,CAAA,EAAG;AACpB,QAAA,MAAM,IAAI,aAAa,mDAAmD,CAAA;AAAA,MAC5E;AACA,MAAA,OAAO,cAAA;AAAA,IACT;AACA,IAAA,OAAO,qBAAA,CAAsB,KAAA,EAAO,UAAA,EAAY,OAAO,CAAA;AAAA,EACzD,CAAA;AAAA,EACA,YAAA,EAAc,CAAC,EAAE,QAAA,EAAU,UAAU,YAAA,EAAc,IAAA,EAAM,OAAM,MAAO;AAAA,IACpE,QAAA,EAAU,UAAA;AAAA,IACV,SAAA,EAAW,QAAA;AAAA,IACX,aAAA,EAAe,YAAA;AAAA,IACf,IAAA;AAAA,IACA,KAAA,EAAO,CAAC,GAAG,KAAK,CAAA;AAAA,IAChB;AAAA,GACF;AACF,CAAA;AAEA,IAAM,eAAA,GAAoD;AAAA,EACxD,cAAA,EAAgB,CAAC,KAAA,KAAU;AACzB,IAAA,IAAI,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG,OAAO,MAAA;AAC/B,IAAA,eAAA,CAAgB,OAAO,UAAU,CAAA;AACjC,IAAA,OAAO,qBAAA,CAAsB,KAAA,EAAO,UAAA,EAAY,MAAM,CAAA;AAAA,EACxD,CAAA;AAAA,EACA,YAAA,EAAc,CAAC,EAAE,QAAA,EAAU,UAAU,YAAA,EAAc,IAAA,EAAM,OAAM,MAAO;AAAA,IACpE,QAAA,EAAU,eAAA;AAAA,IACV,SAAA,EAAW,QAAA;AAAA,IACX,aAAA,EAAe,YAAA;AAAA,IACf,IAAA;AAAA,IACA,KAAA,EAAO,CAAC,GAAG,KAAK,CAAA;AAAA,IAChB;AAAA,GACF;AACF,CAAA;AAEA,IAAM,gBAAA,GAAiD;AAAA,EACrD,cAAA,EAAgB,CAAC,KAAA,KAAU;AACzB,IAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AACtB,MAAA,MAAM,IAAI,aAAa,6CAA6C,CAAA;AAAA,IACtE;AACA,IAAA,eAAA,CAAgB,OAAO,WAAW,CAAA;AAGlC,IAAA,IAAI,KAAA,CAAM,MAAA,IAAU,CAAA,EAAG,OAAO,UAAA;AAC9B,IAAA,OAAO,UAAA,CAAW,KAAK,CAAA,CAAE,MAAA,KAAW,IAAI,MAAA,GAAS,cAAA;AAAA,EACnD,CAAA;AAAA,EACA,YAAA,EAAc,CAAC,EAAE,QAAA,EAAU,UAAU,YAAA,EAAc,IAAA,EAAM,OAAM,MAAO;AAAA,IACpE,QAAA,EAAU,WAAA;AAAA,IACV,SAAA,EAAW,QAAA;AAAA,IACX,aAAA,EAAe,YAAA;AAAA,IACf,IAAA;AAAA,IACA,KAAA,EAAO,CAAC,GAAG,KAAK,CAAA;AAAA,IAChB;AAAA,GACF;AACF,CAAA;AAMA,IAAM,iBAAA,GAAiE;AAAA,EACrE,CAAA,EAAG,QAAA;AAAA,EACH,QAAA,EAAU,eAAA;AAAA,EACV,aAAA,EAAe,eAAA;AAAA,EACf,SAAA,EAAW;AACb,CAAA;AAGO,SAAS,eAAe,KAAA,EAAsC;AACnE,EAAA,OAAO,MAAA,CAAO,SAAA,CAAU,cAAA,CAAe,IAAA,CAAK,mBAAmB,KAAK,CAAA;AACtE;AAIO,IAAM,iBACX,MAAA,CAAO,IAAA,CAAK,iBAAiB,CAAA,CAAE,OAAO,cAAc;AAItD,SAAS,mBAAA,CACP,QAAA,EACA,YAAA,EACA,WAAA,EACQ;AACR,EAAA,IAAI,cAAc,OAAO,YAAA;AACzB,EAAA,MAAM,QAAQ,WAAA,CAAY,IAAA,CAAK,CAAC,UAAA,KAAe,UAAA,CAAW,aAAa,QAAQ,CAAA;AAC/E,EAAA,IAAI,CAAC,KAAA,EAAO;AACV,IAAA,MAAM,IAAI,YAAA;AAAA,MACR,sBAAsB,QAAQ,CAAA,4DAAA;AAAA,KAChC;AAAA,EACF;AACA,EAAA,OAAO,KAAA,CAAM,EAAA;AACf;AAIA,SAAS,YAAA,CACP,OAAA,EACA,QAAA,EACA,MAAA,EACA,SACA,WAAA,EACe;AACf,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,KAAA,IAAS,OAAA,CAAQ,SAAS,EAAC;AAChD,EAAA,OAAO,QAAQ,YAAA,CAAa;AAAA,IAC1B,UAAU,MAAA,CAAO,QAAA;AAAA,IACjB,QAAA,EAAU,MAAA,CAAO,QAAA,IAAY,OAAA,CAAQ,eAAe,KAAK,CAAA;AAAA,IACzD,YAAA,EAAc,mBAAA,CAAoB,QAAA,EAAU,MAAA,CAAO,cAAc,WAAW,CAAA;AAAA,IAC5E,IAAA,EAAM,MAAA,CAAO,IAAA,IAAQ,OAAA,CAAQ,IAAA;AAAA,IAC7B,KAAA,EAAO,MAAM,GAAA,CAAI,CAAC,UAAU,EAAE,QAAA,EAAU,IAAA,CAAK,EAAA,EAAG,CAAE;AAAA,GACnD,CAAA;AACH;AAEA,SAAS,aAAA,CACP,OAAA,EACA,QAAA,EACA,WAAA,EACoB;AACpB,EAAA,MAAM,WAA+B,EAAC;AAEtC,EAAA,IAAI,QAAA,CAAS,CAAA,EAAG,QAAA,CAAS,IAAA,CAAK,YAAA,CAAa,QAAA,EAAU,GAAA,EAAK,QAAA,CAAS,CAAA,EAAG,OAAA,EAAS,WAAW,CAAC,CAAA;AAC3F,EAAA,IAAI,QAAA,CAAS,QAAA;AACX,IAAA,QAAA,CAAS,IAAA,CAAK,aAAa,eAAA,EAAiB,UAAA,EAAY,SAAS,QAAA,EAAU,OAAA,EAAS,WAAW,CAAC,CAAA;AAClG,EAAA,IAAI,QAAA,CAAS,aAAA;AACX,IAAA,QAAA,CAAS,IAAA,CAAK,aAAa,eAAA,EAAiB,eAAA,EAAiB,SAAS,aAAA,EAAe,OAAA,EAAS,WAAW,CAAC,CAAA;AAC5G,EAAA,IAAI,QAAA,CAAS,SAAA;AACX,IAAA,QAAA,CAAS,IAAA,CAAK,aAAa,gBAAA,EAAkB,WAAA,EAAa,SAAS,SAAA,EAAW,OAAA,EAAS,WAAW,CAAC,CAAA;AAErG,EAAA,IAAI,QAAA,CAAS,WAAW,CAAA,EAAG;AACzB,IAAA,MAAM,IAAI,aAAa,mCAAmC,CAAA;AAAA,EAC5D;AACA,EAAA,OAAO,QAAA;AACT;AASO,SAAS,eAAA,CACd,OACA,WAAA,EACiB;AACjB,EAAA,OAAO;AAAA,IACL,YAAY,KAAA,CAAM,SAAA;AAAA,IAClB,SAAS,KAAA,CAAM,OAAA;AAAA,IACf,aAAa,KAAA,CAAM,UAAA;AAAA,IACnB,aAAa,KAAA,CAAM,UAAA;AAAA,IACnB,UAAU,KAAA,CAAM,QAAA;AAAA,IAChB,MAAM,KAAA,CAAM,IAAA,GAAO,CAAC,GAAG,KAAA,CAAM,IAAI,CAAA,GAAI,MAAA;AAAA,IACrC,OAAO,KAAA,CAAM,KAAA;AAAA,IACb,SAAS,KAAA,CAAM,MAAA;AAAA,IACf,QAAA,EAAU,cAAc,KAAA,CAAM,OAAA,IAAW,EAAC,EAAG,KAAA,CAAM,UAAU,WAAW;AAAA,GAC1E;AACF;AAGA,IAAM,qBAAA,GAAwB;AAAA,EAC5B,SAAA;AAAA,EACA,YAAA;AAAA,EACA,YAAA;AAAA,EACA,UAAA;AAAA,EACA,MAAA;AAAA,EACA;AACF,CAAA;AAMO,SAAS,eAAA,CACd,KAAA,EACA,WAAA,GAAwC,EAAC,EACxB;AACjB,EAAA,MAAM,eAAA,GAAkB,sBAAsB,IAAA,CAAK,CAAC,QAAQ,KAAA,CAAM,GAAG,MAAM,MAAS,CAAA;AACpF,EAAA,IAAI,CAAC,KAAA,CAAM,QAAA,IAAY,CAAC,eAAA,EAAiB;AACvC,IAAA,MAAM,IAAI,YAAA;AAAA,MACR;AAAA,KAEF;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,SAAS,KAAA,CAAM,OAAA;AAAA,IACf,aAAa,KAAA,CAAM,UAAA;AAAA,IACnB,aAAa,KAAA,CAAM,UAAA;AAAA,IACnB,UAAU,KAAA,CAAM,QAAA;AAAA,IAChB,MAAM,KAAA,CAAM,IAAA,GAAO,CAAC,GAAG,KAAA,CAAM,IAAI,CAAA,GAAI,MAAA;AAAA,IACrC,OAAO,KAAA,CAAM,KAAA;AAAA,IACb,SAAS,KAAA,CAAM,MAAA;AAAA,IACf,QAAA,EAAU,KAAA,CAAM,QAAA,GACZ,aAAA,CAAc,KAAA,CAAM,OAAA,IAAW,EAAC,EAAG,KAAA,CAAM,QAAA,EAAU,WAAW,CAAA,GAC9D;AAAA,GACN;AACF","file":"index.js","sourcesContent":["import type {\n Connection,\n CreatePostInput,\n MediaResource,\n PostMetadata,\n PostPlatform,\n PostTypeFor,\n PostVariantInput,\n PublishMode,\n SettingsFor,\n UpdatePostInput,\n} from './resources';\n\n/* --------------------------------- types --------------------------------- */\n\n/** A media attachment — the uploaded asset (or just its id + kind). */\nexport type MediaInput = Pick<MediaResource, 'id' | 'kind'>;\n\n/** Base content, shared across channels unless a channel overrides it. */\nexport interface PostContent {\n body?: string;\n media?: readonly MediaInput[];\n}\n\n/**\n * One channel's config. The SDK owns the plumbing (connection, media, assembly)\n * and derives `post_type` from the media; the CUSTOMER owns `settings` — the\n * platform's native, fully-typed config. An Instagram setting on an X channel is\n * a compile error. `postType` is an optional override of the derived value.\n */\nexport interface ChannelConfig<P extends PostPlatform> {\n settings: SettingsFor<P>;\n postType?: PostTypeFor<P>;\n body?: string;\n media?: readonly MediaInput[];\n connectionId?: string;\n}\n\n/** Channels keyed by platform — each value typed to that platform. */\nexport type Channels = { [P in PostPlatform]?: ChannelConfig<P> };\n\nexport interface ComposePostInput {\n profileId: string;\n content?: PostContent;\n channels: Channels;\n publish?: PublishMode;\n scheduleAt?: string;\n externalId?: string;\n metadata?: PostMetadata;\n tags?: readonly string[];\n notes?: string;\n dryRun?: boolean;\n}\n\n/** A post edit. Omit `channels` for a light edit; include it to rebuild variants. */\nexport interface ComposeUpdateInput {\n content?: PostContent;\n channels?: Channels;\n publish?: PublishMode;\n scheduleAt?: string;\n externalId?: string;\n metadata?: PostMetadata;\n tags?: readonly string[];\n notes?: string;\n dryRun?: boolean;\n}\n\n/** The connections a build resolves against (accepts a full `Connection[]`). */\nexport type ConnectionRef = Pick<Connection, 'id' | 'platform'>;\n\n/** Thrown when a post can't be composed (no connection, unsupported media, …). */\nexport class ComposeError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ComposeError';\n }\n}\n\ntype VariantFor<P extends PostPlatform> = Extract<PostVariantInput, { platform: P }>;\n\n/* ----------------------------- platform handlers -------------------------- */\n\n/** What a handler receives once `post_type` is resolved (derived or explicit). */\ninterface ResolvedChannel<P extends PostPlatform> {\n settings: SettingsFor<P>;\n postType: PostTypeFor<P>;\n connectionId: string;\n body: string | undefined;\n media: readonly { media_id: string }[];\n}\n\n/**\n * Each platform implements two layers:\n * - `derivePostType` — the SUGAR: guess `post_type` from the media. Pure; only\n * runs when the caller doesn't pass `postType`.\n * - `buildVariant` — the explicit CORE: assemble the typed variant from a\n * resolved channel. Passes `settings` straight through (the customer owns it).\n * Splitting them keeps the core stable and the derivation purely additive.\n */\ninterface PlatformHandler<P extends PostPlatform> {\n derivePostType(media: readonly MediaInput[]): PostTypeFor<P>;\n buildVariant(resolved: ResolvedChannel<P>): VariantFor<P>;\n}\n\nfunction countKinds(media: readonly MediaInput[]) {\n let images = 0;\n let videos = 0;\n let documents = 0;\n for (const item of media) {\n if (item.kind === 'video') videos += 1;\n else if (item.kind === 'document') documents += 1;\n else images += 1; // image | gif\n }\n return { images, videos, documents };\n}\n\nfunction rejectDocuments(media: readonly MediaInput[], platform: string): void {\n if (countKinds(media).documents > 0) {\n throw new ComposeError(`${platform} does not support document uploads.`);\n }\n}\n\n/**\n * X / LinkedIn / Facebook have no mixed-media placement: a post is text, images,\n * OR a single video — never a blend. Reject the combos that have no valid\n * post_type here (a clear local error), not as a server 422.\n */\nfunction deriveSinglePlacement<V extends 'video' | 'reel'>(\n media: readonly MediaInput[],\n platform: string,\n videoType: V,\n): 'single_image' | 'multi_image' | V {\n const { images, videos } = countKinds(media);\n if (images > 0 && videos > 0) {\n throw new ComposeError(\n `${platform} can't combine images and video in one post — split them into separate posts.`,\n );\n }\n if (videos > 1) {\n throw new ComposeError(`${platform} allows at most one video per post.`);\n }\n if (videos === 1) return videoType;\n return images >= 2 ? 'multi_image' : 'single_image';\n}\n\nconst xHandler: PlatformHandler<'x'> = {\n derivePostType: (media) => {\n if (media.length === 0) return 'text';\n rejectDocuments(media, 'X');\n return deriveSinglePlacement(media, 'X', 'video');\n },\n buildVariant: ({ settings, postType, connectionId, body, media }) => ({\n platform: 'x',\n post_type: postType,\n connection_id: connectionId,\n body,\n media: [...media],\n settings,\n }),\n};\n\nconst linkedInHandler: PlatformHandler<'linkedin'> = {\n derivePostType: (media) => {\n if (media.length === 0) return 'text';\n if (countKinds(media).documents > 0) {\n // A document rides as a `single_image` post_type; the customer sets\n // `content_kind: 'document'` in settings — we don't infer it.\n if (media.length > 1) {\n throw new ComposeError('A LinkedIn document post takes a single document.');\n }\n return 'single_image';\n }\n return deriveSinglePlacement(media, 'LinkedIn', 'video');\n },\n buildVariant: ({ settings, postType, connectionId, body, media }) => ({\n platform: 'linkedin',\n post_type: postType,\n connection_id: connectionId,\n body,\n media: [...media],\n settings,\n }),\n};\n\nconst facebookHandler: PlatformHandler<'facebook_page'> = {\n derivePostType: (media) => {\n if (media.length === 0) return 'text';\n rejectDocuments(media, 'Facebook');\n return deriveSinglePlacement(media, 'Facebook', 'reel');\n },\n buildVariant: ({ settings, postType, connectionId, body, media }) => ({\n platform: 'facebook_page',\n post_type: postType,\n connection_id: connectionId,\n body,\n media: [...media],\n settings,\n }),\n};\n\nconst instagramHandler: PlatformHandler<'instagram'> = {\n derivePostType: (media) => {\n if (media.length === 0) {\n throw new ComposeError('Instagram requires at least one media item.');\n }\n rejectDocuments(media, 'Instagram');\n // 2+ items is always a carousel (the API allows it to mix image/gif/video);\n // a lone item is a reel (video) or a single image.\n if (media.length >= 2) return 'carousel';\n return countKinds(media).videos === 1 ? 'reel' : 'single_image';\n },\n buildVariant: ({ settings, postType, connectionId, body, media }) => ({\n platform: 'instagram',\n post_type: postType,\n connection_id: connectionId,\n body,\n media: [...media],\n settings,\n }),\n};\n\n/**\n * The handler registry. A `Record<PostPlatform, …>` (no optional keys), so a\n * platform the contract defines without a handler here is a COMPILE ERROR.\n */\nconst PLATFORM_HANDLERS: { [P in PostPlatform]: PlatformHandler<P> } = {\n x: xHandler,\n linkedin: linkedInHandler,\n facebook_page: facebookHandler,\n instagram: instagramHandler,\n};\n\n/** Narrow any platform string to a posting platform. */\nexport function isPostPlatform(value: string): value is PostPlatform {\n return Object.prototype.hasOwnProperty.call(PLATFORM_HANDLERS, value);\n}\n\n/** Posting platforms — the single source, derived from the handler registry\n * (the `.filter` type-guard narrows `string[]` → `PostPlatform[]`, cast-free). */\nexport const POST_PLATFORMS: PostPlatform[] =\n Object.keys(PLATFORM_HANDLERS).filter(isPostPlatform);\n\n/* -------------------------------- assembly -------------------------------- */\n\nfunction resolveConnectionId(\n platform: PostPlatform,\n connectionId: string | undefined,\n connections: readonly ConnectionRef[],\n): string {\n if (connectionId) return connectionId;\n const match = connections.find((connection) => connection.platform === platform);\n if (!match) {\n throw new ComposeError(\n `No connection for \"${platform}\" on this profile. Connect the account or pass connectionId.`,\n );\n }\n return match.id;\n}\n\n/** Resolve one channel into its typed variant. Generic over the concrete `P`, so\n * the handler / config / result stay correlated with no cast. */\nfunction buildChannel<P extends PostPlatform>(\n handler: PlatformHandler<P>,\n platform: P,\n config: ChannelConfig<P>,\n content: PostContent,\n connections: readonly ConnectionRef[],\n): VariantFor<P> {\n const media = config.media ?? content.media ?? [];\n return handler.buildVariant({\n settings: config.settings,\n postType: config.postType ?? handler.derivePostType(media),\n connectionId: resolveConnectionId(platform, config.connectionId, connections),\n body: config.body ?? content.body,\n media: media.map((item) => ({ media_id: item.id })),\n });\n}\n\nfunction buildVariants(\n content: PostContent,\n channels: Channels,\n connections: readonly ConnectionRef[],\n): PostVariantInput[] {\n const variants: PostVariantInput[] = [];\n\n if (channels.x) variants.push(buildChannel(xHandler, 'x', channels.x, content, connections));\n if (channels.linkedin)\n variants.push(buildChannel(linkedInHandler, 'linkedin', channels.linkedin, content, connections));\n if (channels.facebook_page)\n variants.push(buildChannel(facebookHandler, 'facebook_page', channels.facebook_page, content, connections));\n if (channels.instagram)\n variants.push(buildChannel(instagramHandler, 'instagram', channels.instagram, content, connections));\n\n if (variants.length === 0) {\n throw new ComposeError('At least one channel is required.');\n }\n return variants;\n}\n\n/**\n * Turn an ergonomic `{ content, channels }` input into the exact\n * `CreatePostInput` the API expects — resolving each channel's connection,\n * attaching the shared/overridden media, and deriving `post_type` from that\n * media. The customer never assembles `variants[]` or sees a `connection_id`,\n * and owns each channel's typed `settings`.\n */\nexport function buildCreatePost(\n input: ComposePostInput,\n connections: readonly ConnectionRef[],\n): CreatePostInput {\n return {\n profile_id: input.profileId,\n publish: input.publish,\n schedule_at: input.scheduleAt,\n external_id: input.externalId,\n metadata: input.metadata,\n tags: input.tags ? [...input.tags] : undefined,\n notes: input.notes,\n dry_run: input.dryRun,\n variants: buildVariants(input.content ?? {}, input.channels, connections),\n };\n}\n\n/** The envelope fields the API counts as a real edit (`dry_run` deliberately not). */\nconst MUTABLE_ENVELOPE_KEYS = [\n 'publish',\n 'scheduleAt',\n 'externalId',\n 'metadata',\n 'tags',\n 'notes',\n] as const satisfies readonly (keyof ComposeUpdateInput)[];\n\n/**\n * Build an `UpdatePostInput`. With `channels`, the full variant set is rebuilt\n * (the API's PATCH replaces it); without it, only the envelope changes.\n */\nexport function buildUpdatePost(\n input: ComposeUpdateInput,\n connections: readonly ConnectionRef[] = [],\n): UpdatePostInput {\n const changesEnvelope = MUTABLE_ENVELOPE_KEYS.some((key) => input[key] !== undefined);\n if (!input.channels && !changesEnvelope) {\n throw new ComposeError(\n 'A post update must change at least one field — pass content/channels or an ' +\n 'envelope field (publish, scheduleAt, tags, …).',\n );\n }\n\n return {\n publish: input.publish,\n schedule_at: input.scheduleAt,\n external_id: input.externalId,\n metadata: input.metadata,\n tags: input.tags ? [...input.tags] : undefined,\n notes: input.notes,\n dry_run: input.dryRun,\n variants: input.channels\n ? buildVariants(input.content ?? {}, input.channels, connections)\n : undefined,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/compose.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type {\n Connection,\n CreatePostInput,\n MediaResource,\n PostMetadata,\n PostPlatform,\n PostTypeFor,\n PostVariantInput,\n PublishMode,\n SettingsFor,\n UpdatePostInput,\n} from './resources';\n\n/* --------------------------------- types --------------------------------- */\n\n/** A media attachment — the uploaded asset (or just its id + kind). */\nexport type MediaInput = Pick<MediaResource, 'id' | 'kind'>;\n\n/** Base content, shared across channels unless a channel overrides it. */\nexport interface PostContent {\n body?: string;\n media?: readonly MediaInput[];\n}\n\n/**\n * One channel's config. The SDK owns the plumbing (connection, media, assembly)\n * and derives `post_type` from the media; the CUSTOMER owns `settings` — the\n * platform's native, fully-typed config. An Instagram setting on an X channel is\n * a compile error. `postType` is an optional override of the derived value.\n */\nexport interface ChannelConfig<P extends PostPlatform> {\n settings: SettingsFor<P>;\n postType?: PostTypeFor<P>;\n body?: string;\n media?: readonly MediaInput[];\n connectionId?: string;\n}\n\n/** Channels keyed by platform — each value typed to that platform. */\nexport type Channels = { [P in PostPlatform]?: ChannelConfig<P> };\n\nexport interface ComposePostInput {\n profileId: string;\n content?: PostContent;\n channels: Channels;\n publish?: PublishMode;\n scheduleAt?: string;\n externalId?: string;\n metadata?: PostMetadata;\n tags?: readonly string[];\n notes?: string;\n dryRun?: boolean;\n}\n\n/** A post edit. Omit `channels` for a light edit; include it to rebuild variants. */\nexport interface ComposeUpdateInput {\n content?: PostContent;\n channels?: Channels;\n publish?: PublishMode;\n scheduleAt?: string;\n externalId?: string;\n metadata?: PostMetadata;\n tags?: readonly string[];\n notes?: string;\n dryRun?: boolean;\n}\n\n/** The connections a build resolves against (accepts a full `Connection[]`). */\nexport type ConnectionRef = Pick<Connection, 'id' | 'platform'>;\n\n/** Thrown when a post can't be composed (no connection, unsupported media, …). */\nexport class ComposeError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ComposeError';\n }\n}\n\ntype VariantFor<P extends PostPlatform> = Extract<PostVariantInput, { platform: P }>;\n\n/* ----------------------------- platform handlers -------------------------- */\n\n/** What a handler receives once `post_type` is resolved (derived or explicit). */\ninterface ResolvedChannel<P extends PostPlatform> {\n settings: SettingsFor<P>;\n postType: PostTypeFor<P>;\n connectionId: string;\n body: string | undefined;\n media: readonly { media_id: string }[];\n}\n\n/**\n * Each platform implements two layers:\n * - `derivePostType` — the SUGAR: guess `post_type` from the media. Pure; only\n * runs when the caller doesn't pass `postType`.\n * - `buildVariant` — the explicit CORE: assemble the typed variant from a\n * resolved channel. Passes `settings` straight through (the customer owns it).\n * Splitting them keeps the core stable and the derivation purely additive.\n */\ninterface PlatformHandler<P extends PostPlatform> {\n derivePostType(media: readonly MediaInput[]): PostTypeFor<P>;\n buildVariant(resolved: ResolvedChannel<P>): VariantFor<P>;\n}\n\nfunction countKinds(media: readonly MediaInput[]) {\n let images = 0;\n let videos = 0;\n let documents = 0;\n for (const item of media) {\n if (item.kind === 'video') videos += 1;\n else if (item.kind === 'document') documents += 1;\n else images += 1; // image | gif\n }\n return { images, videos, documents };\n}\n\nfunction rejectDocuments(media: readonly MediaInput[], platform: string): void {\n if (countKinds(media).documents > 0) {\n throw new ComposeError(`${platform} does not support document uploads.`);\n }\n}\n\n/**\n * X / LinkedIn / Facebook have no mixed-media placement: a post is text, images,\n * OR a single video — never a blend. Reject the combos that have no valid\n * post_type here (a clear local error), not as a server 422.\n */\nfunction deriveSinglePlacement<V extends 'video' | 'reel'>(\n media: readonly MediaInput[],\n platform: string,\n videoType: V,\n): 'single_image' | 'multi_image' | V {\n const { images, videos } = countKinds(media);\n if (images > 0 && videos > 0) {\n throw new ComposeError(\n `${platform} can't combine images and video in one post — split them into separate posts.`,\n );\n }\n if (videos > 1) {\n throw new ComposeError(`${platform} allows at most one video per post.`);\n }\n if (videos === 1) return videoType;\n return images >= 2 ? 'multi_image' : 'single_image';\n}\n\nconst xHandler: PlatformHandler<'x'> = {\n derivePostType: (media) => {\n if (media.length === 0) return 'text';\n rejectDocuments(media, 'X');\n return deriveSinglePlacement(media, 'X', 'video');\n },\n buildVariant: ({ settings, postType, connectionId, body, media }) => ({\n platform: 'x',\n post_type: postType,\n connection_id: connectionId,\n body,\n media: [...media],\n settings,\n }),\n};\n\nconst linkedInHandler: PlatformHandler<'linkedin'> = {\n derivePostType: (media) => {\n if (media.length === 0) return 'text';\n if (countKinds(media).documents > 0) {\n // A document rides as a `single_image` post_type; the customer sets\n // `content_kind: 'document'` in settings — we don't infer it.\n if (media.length > 1) {\n throw new ComposeError('A LinkedIn document post takes a single document.');\n }\n return 'single_image';\n }\n return deriveSinglePlacement(media, 'LinkedIn', 'video');\n },\n buildVariant: ({ settings, postType, connectionId, body, media }) => ({\n platform: 'linkedin',\n post_type: postType,\n connection_id: connectionId,\n body,\n media: [...media],\n settings,\n }),\n};\n\nconst facebookHandler: PlatformHandler<'facebook_page'> = {\n derivePostType: (media) => {\n if (media.length === 0) return 'text';\n rejectDocuments(media, 'Facebook');\n return deriveSinglePlacement(media, 'Facebook', 'reel');\n },\n buildVariant: ({ settings, postType, connectionId, body, media }) => ({\n platform: 'facebook_page',\n post_type: postType,\n connection_id: connectionId,\n body,\n media: [...media],\n settings,\n }),\n};\n\nconst instagramHandler: PlatformHandler<'instagram'> = {\n derivePostType: (media) => {\n if (media.length === 0) {\n throw new ComposeError('Instagram requires at least one media item.');\n }\n rejectDocuments(media, 'Instagram');\n // 2+ items is always a carousel (the API allows it to mix image/gif/video);\n // a lone item is a reel (video) or a single image.\n if (media.length >= 2) return 'carousel';\n return countKinds(media).videos === 1 ? 'reel' : 'single_image';\n },\n buildVariant: ({ settings, postType, connectionId, body, media }) => ({\n platform: 'instagram',\n post_type: postType,\n connection_id: connectionId,\n body,\n media: [...media],\n settings,\n }),\n};\n\nconst tiktokHandler: PlatformHandler<'tiktok'> = {\n derivePostType: (media) => {\n if (media.length === 0) {\n throw new ComposeError('TikTok requires at least one media item.');\n }\n rejectDocuments(media, 'TikTok');\n const { images, videos } = countKinds(media);\n // TikTok keeps video and photo posts separate: a video post is one standalone\n // video; photos form a single image or a multi-photo carousel. No blend.\n if (images > 0 && videos > 0) {\n throw new ComposeError(\n \"TikTok can't combine images and video in one post — split them into separate posts.\",\n );\n }\n if (videos > 1) {\n throw new ComposeError('TikTok allows at most one video per post.');\n }\n if (videos === 1) return 'video';\n return images >= 2 ? 'carousel' : 'single_image';\n },\n buildVariant: ({ settings, postType, connectionId, body, media }) => ({\n platform: 'tiktok',\n post_type: postType,\n connection_id: connectionId,\n body,\n media: [...media],\n settings,\n }),\n};\n\n/**\n * The handler registry. A `Record<PostPlatform, …>` (no optional keys), so a\n * platform the contract defines without a handler here is a COMPILE ERROR.\n */\nconst PLATFORM_HANDLERS: { [P in PostPlatform]: PlatformHandler<P> } = {\n x: xHandler,\n linkedin: linkedInHandler,\n facebook_page: facebookHandler,\n instagram: instagramHandler,\n tiktok: tiktokHandler,\n};\n\n/** Narrow any platform string to a posting platform. */\nexport function isPostPlatform(value: string): value is PostPlatform {\n return Object.prototype.hasOwnProperty.call(PLATFORM_HANDLERS, value);\n}\n\n/** Posting platforms — the single source, derived from the handler registry\n * (the `.filter` type-guard narrows `string[]` → `PostPlatform[]`, cast-free). */\nexport const POST_PLATFORMS: PostPlatform[] =\n Object.keys(PLATFORM_HANDLERS).filter(isPostPlatform);\n\n/* -------------------------------- assembly -------------------------------- */\n\nfunction resolveConnectionId(\n platform: PostPlatform,\n connectionId: string | undefined,\n connections: readonly ConnectionRef[],\n): string {\n if (connectionId) return connectionId;\n const match = connections.find((connection) => connection.platform === platform);\n if (!match) {\n throw new ComposeError(\n `No connection for \"${platform}\" on this profile. Connect the account or pass connectionId.`,\n );\n }\n return match.id;\n}\n\n/** Resolve one channel into its typed variant. Generic over the concrete `P`, so\n * the handler / config / result stay correlated with no cast. */\nfunction buildChannel<P extends PostPlatform>(\n handler: PlatformHandler<P>,\n platform: P,\n config: ChannelConfig<P>,\n content: PostContent,\n connections: readonly ConnectionRef[],\n): VariantFor<P> {\n const media = config.media ?? content.media ?? [];\n return handler.buildVariant({\n settings: config.settings,\n postType: config.postType ?? handler.derivePostType(media),\n connectionId: resolveConnectionId(platform, config.connectionId, connections),\n body: config.body ?? content.body,\n media: media.map((item) => ({ media_id: item.id })),\n });\n}\n\n/**\n * Resolve ONE platform's channel, or `undefined` when the caller didn't include\n * it. Generic over the concrete `P`, so `channels[platform]` and\n * `PLATFORM_HANDLERS[platform]` stay correlated to the same platform with no cast.\n * Wrapping the registry lookup in this generic is what lets `buildVariants`\n * iterate `POST_PLATFORMS` (a union) without the per-member variance error.\n */\nfunction collectChannel<P extends PostPlatform>(\n platform: P,\n channels: Channels,\n content: PostContent,\n connections: readonly ConnectionRef[],\n): VariantFor<P> | undefined {\n const config = channels[platform];\n if (!config) return undefined;\n return buildChannel(\n PLATFORM_HANDLERS[platform],\n platform,\n config,\n content,\n connections,\n );\n}\n\nfunction buildVariants(\n content: PostContent,\n channels: Channels,\n connections: readonly ConnectionRef[],\n): PostVariantInput[] {\n // Driven by POST_PLATFORMS (derived from the exhaustive PLATFORM_HANDLERS), so a\n // new platform added to the registry is dispatched here automatically — no\n // hand-maintained per-platform branch to forget (the gap that once dropped TikTok).\n const variants = POST_PLATFORMS.flatMap((platform) => {\n const variant = collectChannel(platform, channels, content, connections);\n return variant ? [variant] : [];\n });\n\n if (variants.length === 0) {\n throw new ComposeError('At least one channel is required.');\n }\n return variants;\n}\n\n/**\n * Turn an ergonomic `{ content, channels }` input into the exact\n * `CreatePostInput` the API expects — resolving each channel's connection,\n * attaching the shared/overridden media, and deriving `post_type` from that\n * media. The customer never assembles `variants[]` or sees a `connection_id`,\n * and owns each channel's typed `settings`.\n */\nexport function buildCreatePost(\n input: ComposePostInput,\n connections: readonly ConnectionRef[],\n): CreatePostInput {\n return {\n profile_id: input.profileId,\n publish: input.publish,\n schedule_at: input.scheduleAt,\n external_id: input.externalId,\n metadata: input.metadata,\n tags: input.tags ? [...input.tags] : undefined,\n notes: input.notes,\n dry_run: input.dryRun,\n variants: buildVariants(input.content ?? {}, input.channels, connections),\n };\n}\n\n/** The envelope fields the API counts as a real edit (`dry_run` deliberately not). */\nconst MUTABLE_ENVELOPE_KEYS = [\n 'publish',\n 'scheduleAt',\n 'externalId',\n 'metadata',\n 'tags',\n 'notes',\n] as const satisfies readonly (keyof ComposeUpdateInput)[];\n\n/**\n * Build an `UpdatePostInput`. With `channels`, the full variant set is rebuilt\n * (the API's PATCH replaces it); without it, only the envelope changes.\n */\nexport function buildUpdatePost(\n input: ComposeUpdateInput,\n connections: readonly ConnectionRef[] = [],\n): UpdatePostInput {\n const changesEnvelope = MUTABLE_ENVELOPE_KEYS.some((key) => input[key] !== undefined);\n if (!input.channels && !changesEnvelope) {\n throw new ComposeError(\n 'A post update must change at least one field — pass content/channels or an ' +\n 'envelope field (publish, scheduleAt, tags, …).',\n );\n }\n\n return {\n publish: input.publish,\n schedule_at: input.scheduleAt,\n external_id: input.externalId,\n metadata: input.metadata,\n tags: input.tags ? [...input.tags] : undefined,\n notes: input.notes,\n dry_run: input.dryRun,\n variants: input.channels\n ? buildVariants(input.content ?? {}, input.channels, connections)\n : undefined,\n };\n}\n"]}
|