Skip to content

Commit aade583

Browse files
Support channels in provisioner (#1412)
* feat(deploy): support channels in project state Add ChannelSpec/ChannelState types, merge channels in mergeSpecIntoState and mergeProjectPayloadIntoState, include them in toProjectPayload (drop when empty), read them in getStateFromProjectPayload, and accept null channels in the YAML validator. * feat(project,lexicon): propagate channels through parse, serialize, merge Add Provisioner.Channel type and channels field on Provisioner.Project_v1 in lexicon. Carry channels through Project, from-app-state, to-app-state, to-project, and merge-project (REPLACE mode prefers source, falls back to target; baseMerge treats channels as opaque like collections). * add collections to portability projectspec * style(deploy): format stateTransform with prettier * move channel typedef into portability layer * add extra tests * tweak typings * changesets * types --------- Co-authored-by: Joe Clark <jclark@openfn.org>
1 parent 13464e0 commit aade583

19 files changed

Lines changed: 603 additions & 4 deletions

.changeset/dull-meals-stare.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@openfn/lexicon': minor
3+
'@openfn/project': minor
4+
'@openfn/deploy': minor
5+
'@openfn/cli': minor
6+
---
7+
8+
Add support for channels

packages/deploy/src/stateTransform.ts

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,58 @@ export function mergeSpecIntoState(
374374
}
375375
)
376376
);
377+
const nextChannels = Object.fromEntries(
378+
splitZip(oldState.channels || {}, spec.channels || {}).map(
379+
([channelKey, stateChannel, specChannel]) => {
380+
if (specChannel && !stateChannel) {
381+
return [
382+
channelKey,
383+
{
384+
id: crypto.randomUUID(),
385+
name: specChannel.name,
386+
destination_url: specChannel.destination_url,
387+
enabled: specChannel.enabled,
388+
destination_credential_id:
389+
specChannel.destination_credential &&
390+
getStateJobCredential(
391+
specChannel.destination_credential,
392+
nextCredentials
393+
),
394+
},
395+
];
396+
}
397+
398+
if (specChannel && stateChannel) {
399+
return [
400+
channelKey,
401+
{
402+
id: stateChannel.id,
403+
name: specChannel.name,
404+
destination_url: specChannel.destination_url,
405+
enabled: specChannel.enabled,
406+
destination_credential_id:
407+
specChannel.destination_credential &&
408+
getStateJobCredential(
409+
specChannel.destination_credential,
410+
nextCredentials
411+
),
412+
},
413+
];
414+
}
415+
416+
if (!specChannel && stateChannel) {
417+
return [channelKey, { id: stateChannel.id, delete: true }];
418+
}
419+
420+
throw new DeployError(
421+
`Invalid channel spec or corrupted state for channel: ${
422+
stateChannel?.name || specChannel?.name
423+
}`,
424+
'VALIDATION_ERROR'
425+
);
426+
}
427+
)
428+
);
377429

378430
const nextWorkflows = Object.fromEntries(
379431
splitZip(oldState.workflows, spec.workflows).map(
@@ -442,6 +494,7 @@ export function mergeSpecIntoState(
442494
workflows: nextWorkflows,
443495
project_credentials: nextCredentials,
444496
collections: nextCollections,
497+
channels: nextChannels,
445498
};
446499

447500
if (spec.description) projectState.description = spec.description;
@@ -490,9 +543,12 @@ export function getStateFromProjectPayload(
490543

491544
const collections = reduceByKey('name', project.collections || []);
492545

546+
const channels = reduceByKey('name', project.channels || []);
547+
493548
return {
494549
...project,
495550
collections,
551+
channels,
496552
project_credentials,
497553
workflows,
498554
};
@@ -561,9 +617,18 @@ export function mergeProjectPayloadIntoState(
561617
)
562618
);
563619

620+
const nextChannels = Object.fromEntries(
621+
idKeyPairs(project.channels || [], state.channels || {}).map(
622+
([key, nextChannel, _state]) => {
623+
return [key, nextChannel];
624+
}
625+
)
626+
);
627+
564628
return {
565629
...project,
566630
collections: nextCollections,
631+
channels: nextChannels,
567632
project_credentials: nextCredentials,
568633
workflows: nextWorkflows,
569634
};
@@ -609,12 +674,17 @@ export function toProjectPayload(state: ProjectState): ProjectPayload {
609674
state.collections
610675
);
611676

612-
const { collections: _, ...stateWithoutCollections } = state;
677+
const channels: ProjectPayload['channels'] = Object.values(
678+
state.channels || {}
679+
);
680+
681+
const { collections: _, channels: __, ...stateWithoutOptionals } = state;
613682

614683
return {
615-
...stateWithoutCollections,
684+
...stateWithoutOptionals,
616685
project_credentials,
617686
workflows,
618687
...(collections.length > 0 && { collections }),
688+
...(channels.length > 0 && { channels }),
619689
};
620690
}

packages/deploy/src/types.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,29 @@ export type CollectionState = {
117117
delete?: boolean;
118118
};
119119

120+
export type ChannelSpec = {
121+
name: string;
122+
destination_url: string;
123+
enabled: boolean;
124+
destination_credential: string | null;
125+
};
126+
127+
export type ChannelState = {
128+
id: string;
129+
name: string;
130+
destination_url: string;
131+
enabled: boolean;
132+
destination_credential_id: string | null;
133+
delete?: boolean;
134+
};
135+
120136
export interface ProjectSpec {
121137
name: string;
122138
description: string;
123139
workflows: Record<string | symbol, WorkflowSpec>;
124140
credentials: Record<string | symbol, CredentialSpec>;
125141
collections: Record<string | symbol, CollectionSpec>;
142+
channels: Record<string | symbol, ChannelSpec>;
126143
}
127144

128145
export interface WorkflowState {
@@ -146,13 +163,15 @@ export interface ProjectState {
146163
workflows: Record<string | symbol, WorkflowState>;
147164
project_credentials: Record<string | symbol, CredentialState>;
148165
collections: Record<string | symbol, CollectionState>;
166+
channels: Record<string | symbol, ChannelState>;
149167
}
150168

151169
export interface ProjectPayload {
152170
id: string;
153171
name: string;
154172
description: string;
155173
collections?: Concrete<CollectionState>[];
174+
channels?: Concrete<ChannelState>[];
156175
project_credentials: Concrete<CredentialState>[];
157176
workflows: {
158177
id: string;

packages/deploy/src/validator.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ export async function parseAndValidate(
129129
}
130130
}
131131

132+
if (pair.key && pair.key.value === 'channels') {
133+
if (pair.value.value === null) {
134+
return doc.createPair('channels', {});
135+
}
136+
}
137+
132138
if (pair.key && pair.key.value === 'jobs') {
133139
if (pair.value.value === null) {
134140
errors.push({

packages/deploy/test/fixtures.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export function fullExampleSpec() {
55
name: 'my project',
66
description: 'some helpful description',
77
collections: {},
8+
channels: {},
89
credentials: {},
910
workflows: {
1011
'workflow-one': {
@@ -58,6 +59,7 @@ export function fullExampleState() {
5859
name: 'my project',
5960
description: 'some helpful description',
6061
collections: {},
62+
channels: {},
6163
project_credentials: {},
6264
workflows: {
6365
'workflow-one': {
@@ -254,6 +256,7 @@ export const lightningProjectState = {
254256
name: 'collection-one',
255257
},
256258
},
259+
channels: {},
257260
project_credentials: {
258261
'email@test.com-Basic-Auth': {
259262
id: '25f48989-d349-4eb8-99c3-923ebba5b116',

0 commit comments

Comments
 (0)