@postrun/js 0.1.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/index.js ADDED
@@ -0,0 +1,194 @@
1
+ export { PostrunError, connectionsConnect, connectionsDelete, connectionsGet, connectionsListAccounts, connectionsListByProfile, connectionsSelect, createPostrunClient, googleCreateAd, googleCreateAdGroup, googleCreateBudget, googleCreateCampaign, googleCreateConversionAction, googleCreateDisplayAd, googleCreateKeyword, googleDeleteAd, googleDeleteAdGroup, googleDeleteBudget, googleDeleteCampaign, googleDeleteKeyword, googleEditAdGroup, googleEditBudget, googleEditCampaign, googleEnableAd, googleEnableAdGroup, googleEnableCampaign, googleEnableKeyword, googleGetAccount, googleGetAd, googleGetAdGroup, googleGetCampaign, googleGetConversionAction, googleGetInsights, googleGetKeyword, googleListAdGroups, googleListAds, googleListCampaigns, googleListConversionActions, googleListConversionGoals, googleListKeywords, googlePauseAd, googlePauseAdGroup, googlePauseCampaign, googlePauseKeyword, googleRunGaql, googleSetAdGroupBids, googleSetConversionGoal, googleSetKeywordBid, googleUploadConversions, googleUploadImageAsset, logsGet, logsList, mediaCreate, mediaDelete, mediaGet, mediaUpdate, metaAccount, metaAd, metaAds, metaAdset, metaAdsets, metaCampaign, metaCampaigns, metaInsights, postsCreate, postsDelete, postsGet, postsList, postsUpdate, profilesCreate, profilesDelete, profilesGet, profilesList, profilesUpdate, tokensMint, webhooksCreateEndpoint, webhooksCreatePortal, webhooksDeleteEndpoint, webhooksGetEndpoint, webhooksListEndpoints, webhooksListEventTypes, webhooksPing, webhooksUpdateEndpoint } from './chunk-A545VJ5X.js';
2
+ import './chunk-IMU3SG45.js';
3
+
4
+ // src/compose.ts
5
+ var ComposeError = class extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "ComposeError";
9
+ }
10
+ };
11
+ function countKinds(media) {
12
+ let images = 0;
13
+ let videos = 0;
14
+ let documents = 0;
15
+ for (const item of media) {
16
+ if (item.kind === "video") videos += 1;
17
+ else if (item.kind === "document") documents += 1;
18
+ else images += 1;
19
+ }
20
+ return { images, videos, documents };
21
+ }
22
+ function rejectDocuments(media, platform) {
23
+ if (countKinds(media).documents > 0) {
24
+ throw new ComposeError(`${platform} does not support document uploads.`);
25
+ }
26
+ }
27
+ function deriveSinglePlacement(media, platform, videoType) {
28
+ const { images, videos } = countKinds(media);
29
+ if (images > 0 && videos > 0) {
30
+ throw new ComposeError(
31
+ `${platform} can't combine images and video in one post \u2014 split them into separate posts.`
32
+ );
33
+ }
34
+ if (videos > 1) {
35
+ throw new ComposeError(`${platform} allows at most one video per post.`);
36
+ }
37
+ if (videos === 1) return videoType;
38
+ return images >= 2 ? "multi_image" : "single_image";
39
+ }
40
+ var xHandler = {
41
+ derivePostType: (media) => {
42
+ if (media.length === 0) return "text";
43
+ rejectDocuments(media, "X");
44
+ return deriveSinglePlacement(media, "X", "video");
45
+ },
46
+ buildVariant: ({ settings, postType, connectionId, body, media }) => ({
47
+ platform: "x",
48
+ post_type: postType,
49
+ connection_id: connectionId,
50
+ body,
51
+ media: [...media],
52
+ settings
53
+ })
54
+ };
55
+ var linkedInHandler = {
56
+ derivePostType: (media) => {
57
+ if (media.length === 0) return "text";
58
+ if (countKinds(media).documents > 0) {
59
+ if (media.length > 1) {
60
+ throw new ComposeError("A LinkedIn document post takes a single document.");
61
+ }
62
+ return "single_image";
63
+ }
64
+ return deriveSinglePlacement(media, "LinkedIn", "video");
65
+ },
66
+ buildVariant: ({ settings, postType, connectionId, body, media }) => ({
67
+ platform: "linkedin",
68
+ post_type: postType,
69
+ connection_id: connectionId,
70
+ body,
71
+ media: [...media],
72
+ settings
73
+ })
74
+ };
75
+ var facebookHandler = {
76
+ derivePostType: (media) => {
77
+ if (media.length === 0) return "text";
78
+ rejectDocuments(media, "Facebook");
79
+ return deriveSinglePlacement(media, "Facebook", "reel");
80
+ },
81
+ buildVariant: ({ settings, postType, connectionId, body, media }) => ({
82
+ platform: "facebook_page",
83
+ post_type: postType,
84
+ connection_id: connectionId,
85
+ body,
86
+ media: [...media],
87
+ settings
88
+ })
89
+ };
90
+ var instagramHandler = {
91
+ derivePostType: (media) => {
92
+ if (media.length === 0) {
93
+ throw new ComposeError("Instagram requires at least one media item.");
94
+ }
95
+ rejectDocuments(media, "Instagram");
96
+ if (media.length >= 2) return "carousel";
97
+ return countKinds(media).videos === 1 ? "reel" : "single_image";
98
+ },
99
+ buildVariant: ({ settings, postType, connectionId, body, media }) => ({
100
+ platform: "instagram",
101
+ post_type: postType,
102
+ connection_id: connectionId,
103
+ body,
104
+ media: [...media],
105
+ settings
106
+ })
107
+ };
108
+ var PLATFORM_HANDLERS = {
109
+ x: xHandler,
110
+ linkedin: linkedInHandler,
111
+ facebook_page: facebookHandler,
112
+ instagram: instagramHandler
113
+ };
114
+ function isPostPlatform(value) {
115
+ return Object.prototype.hasOwnProperty.call(PLATFORM_HANDLERS, value);
116
+ }
117
+ var POST_PLATFORMS = Object.keys(PLATFORM_HANDLERS).filter(isPostPlatform);
118
+ function resolveConnectionId(platform, connectionId, connections) {
119
+ if (connectionId) return connectionId;
120
+ const match = connections.find((connection) => connection.platform === platform);
121
+ if (!match) {
122
+ throw new ComposeError(
123
+ `No connection for "${platform}" on this profile. Connect the account or pass connectionId.`
124
+ );
125
+ }
126
+ return match.id;
127
+ }
128
+ function buildChannel(handler, platform, config, content, connections) {
129
+ const media = config.media ?? content.media ?? [];
130
+ return handler.buildVariant({
131
+ settings: config.settings,
132
+ postType: config.postType ?? handler.derivePostType(media),
133
+ connectionId: resolveConnectionId(platform, config.connectionId, connections),
134
+ body: config.body ?? content.body,
135
+ media: media.map((item) => ({ media_id: item.id }))
136
+ });
137
+ }
138
+ function buildVariants(content, channels, connections) {
139
+ const variants = [];
140
+ if (channels.x) variants.push(buildChannel(xHandler, "x", channels.x, content, connections));
141
+ if (channels.linkedin)
142
+ variants.push(buildChannel(linkedInHandler, "linkedin", channels.linkedin, content, connections));
143
+ if (channels.facebook_page)
144
+ variants.push(buildChannel(facebookHandler, "facebook_page", channels.facebook_page, content, connections));
145
+ if (channels.instagram)
146
+ variants.push(buildChannel(instagramHandler, "instagram", channels.instagram, content, connections));
147
+ if (variants.length === 0) {
148
+ throw new ComposeError("At least one channel is required.");
149
+ }
150
+ return variants;
151
+ }
152
+ function buildCreatePost(input, connections) {
153
+ return {
154
+ profile_id: input.profileId,
155
+ publish: input.publish,
156
+ schedule_at: input.scheduleAt,
157
+ external_id: input.externalId,
158
+ metadata: input.metadata,
159
+ tags: input.tags ? [...input.tags] : void 0,
160
+ notes: input.notes,
161
+ dry_run: input.dryRun,
162
+ variants: buildVariants(input.content ?? {}, input.channels, connections)
163
+ };
164
+ }
165
+ var MUTABLE_ENVELOPE_KEYS = [
166
+ "publish",
167
+ "scheduleAt",
168
+ "externalId",
169
+ "metadata",
170
+ "tags",
171
+ "notes"
172
+ ];
173
+ function buildUpdatePost(input, connections = []) {
174
+ const changesEnvelope = MUTABLE_ENVELOPE_KEYS.some((key) => input[key] !== void 0);
175
+ if (!input.channels && !changesEnvelope) {
176
+ throw new ComposeError(
177
+ "A post update must change at least one field \u2014 pass content/channels or an envelope field (publish, scheduleAt, tags, \u2026)."
178
+ );
179
+ }
180
+ return {
181
+ publish: input.publish,
182
+ schedule_at: input.scheduleAt,
183
+ external_id: input.externalId,
184
+ metadata: input.metadata,
185
+ tags: input.tags ? [...input.tags] : void 0,
186
+ notes: input.notes,
187
+ dry_run: input.dryRun,
188
+ variants: input.channels ? buildVariants(input.content ?? {}, input.channels, connections) : void 0
189
+ };
190
+ }
191
+
192
+ export { ComposeError, POST_PLATFORMS, buildCreatePost, buildUpdatePost, isPostPlatform };
193
+ //# sourceMappingURL=index.js.map
194
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
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\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"]}