great 1.0.2 → 1.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.
Files changed (180) hide show
  1. package/README.md +141 -43
  2. package/dist/accounts/create.d.ts +1 -1
  3. package/dist/accounts/create.js +2 -4
  4. package/dist/accounts/create.js.map +1 -1
  5. package/dist/accounts/index.d.ts +1 -1
  6. package/dist/accounts/index.js +2 -4
  7. package/dist/accounts/index.js.map +1 -1
  8. package/dist/clone/index.d.ts +4 -2
  9. package/dist/clone/index.js +19 -43
  10. package/dist/clone/index.js.map +1 -1
  11. package/dist/config/index.js +4 -30
  12. package/dist/config/index.js.map +1 -1
  13. package/dist/deploy/index.d.ts +1 -1
  14. package/dist/deploy/index.js +55 -15
  15. package/dist/deploy/index.js.map +1 -1
  16. package/dist/env/index.d.ts +12 -0
  17. package/dist/env/index.js +131 -0
  18. package/dist/env/index.js.map +1 -0
  19. package/dist/explode/transform.d.ts +1 -1
  20. package/dist/explode/transform.js +6 -2
  21. package/dist/explode/transform.js.map +1 -1
  22. package/dist/explode/unescapeMongo.js +9 -1
  23. package/dist/explode/unescapeMongo.js.map +1 -1
  24. package/dist/generated/operations.d.ts +617 -0
  25. package/dist/generated/operations.js +2 -0
  26. package/dist/generated/operations.js.map +1 -0
  27. package/dist/generated/schema.d.ts +965 -0
  28. package/dist/generated/{graphql.js → schema.js} +20 -2
  29. package/dist/generated/schema.js.map +1 -0
  30. package/dist/implode/escapeMongo.d.ts +1 -0
  31. package/dist/implode/escapeMongo.js +23 -0
  32. package/dist/implode/escapeMongo.js.map +1 -0
  33. package/dist/implode/index.d.ts +6 -0
  34. package/dist/implode/index.js +132 -0
  35. package/dist/implode/index.js.map +1 -0
  36. package/dist/implode/transform.d.ts +45 -0
  37. package/dist/implode/transform.js +73 -0
  38. package/dist/implode/transform.js.map +1 -0
  39. package/dist/import/index.d.ts +8 -0
  40. package/dist/import/index.js +43 -0
  41. package/dist/import/index.js.map +1 -0
  42. package/dist/include/index.d.ts +10 -0
  43. package/dist/include/index.js +41 -0
  44. package/dist/include/index.js.map +1 -0
  45. package/dist/index.js +15 -2
  46. package/dist/index.js.map +1 -1
  47. package/dist/init/index.js +8 -16
  48. package/dist/init/index.js.map +1 -1
  49. package/dist/integrations/create.d.ts +1 -1
  50. package/dist/integrations/create.js +3 -5
  51. package/dist/integrations/create.js.map +1 -1
  52. package/dist/integrations/list.d.ts +1 -1
  53. package/dist/integrations/list.js +3 -5
  54. package/dist/integrations/list.js.map +1 -1
  55. package/dist/migrate/index.d.ts +5 -0
  56. package/dist/migrate/index.js +101 -0
  57. package/dist/migrate/index.js.map +1 -0
  58. package/dist/pull/index.d.ts +4 -2
  59. package/dist/pull/index.js +25 -44
  60. package/dist/pull/index.js.map +1 -1
  61. package/dist/push/index.d.ts +1 -1
  62. package/dist/push/index.js +364 -93
  63. package/dist/push/index.js.map +1 -1
  64. package/dist/rename/index.d.ts +8 -0
  65. package/dist/rename/index.js +109 -0
  66. package/dist/rename/index.js.map +1 -0
  67. package/dist/repl/dispatch.d.ts +8 -2
  68. package/dist/repl/dispatch.js +43 -29
  69. package/dist/repl/dispatch.js.map +1 -1
  70. package/dist/repl/dispatchAndLoad.d.ts +7 -0
  71. package/dist/repl/dispatchAndLoad.js +35 -0
  72. package/dist/repl/dispatchAndLoad.js.map +1 -0
  73. package/dist/repl/help.js +13 -2
  74. package/dist/repl/help.js.map +1 -1
  75. package/dist/repl/index.d.ts +1 -1
  76. package/dist/repl/index.js +31 -10
  77. package/dist/repl/index.js.map +1 -1
  78. package/dist/repl/load.d.ts +8 -0
  79. package/dist/repl/load.js +53 -0
  80. package/dist/repl/load.js.map +1 -0
  81. package/dist/run/index.d.ts +7 -5
  82. package/dist/run/index.js +5 -7
  83. package/dist/run/index.js.map +1 -1
  84. package/dist/status/index.d.ts +1 -1
  85. package/dist/status/index.js +6 -8
  86. package/dist/status/index.js.map +1 -1
  87. package/dist/utils/config.d.ts +9 -2
  88. package/dist/utils/config.js +39 -6
  89. package/dist/utils/config.js.map +1 -1
  90. package/dist/utils/createIntegrationFromFile.d.ts +3 -1
  91. package/dist/utils/createIntegrationFromFile.js +14 -30
  92. package/dist/utils/createIntegrationFromFile.js.map +1 -1
  93. package/dist/utils/detectPullConflicts.d.ts +8 -0
  94. package/dist/utils/detectPullConflicts.js +65 -0
  95. package/dist/utils/detectPullConflicts.js.map +1 -0
  96. package/dist/utils/dispatch.d.ts +2 -2
  97. package/dist/utils/dispatch.js +5 -4
  98. package/dist/utils/dispatch.js.map +1 -1
  99. package/dist/utils/elementFiles.d.ts +13 -0
  100. package/dist/utils/elementFiles.js +76 -0
  101. package/dist/utils/elementFiles.js.map +1 -0
  102. package/dist/utils/fetchIntegrationDefinition.d.ts +8 -0
  103. package/dist/utils/fetchIntegrationDefinition.js +58 -0
  104. package/dist/utils/fetchIntegrationDefinition.js.map +1 -0
  105. package/dist/utils/fetchLocal.d.ts +3 -1
  106. package/dist/utils/fetchLocal.js +11 -9
  107. package/dist/utils/fetchLocal.js.map +1 -1
  108. package/dist/utils/fetchSession.d.ts +1 -1
  109. package/dist/utils/fetchSession.js +2 -1
  110. package/dist/utils/fetchSession.js.map +1 -1
  111. package/dist/utils/graphqlClient.d.ts +3 -0
  112. package/dist/utils/graphqlClient.js +3 -0
  113. package/dist/utils/graphqlClient.js.map +1 -1
  114. package/dist/utils/graphqlError.d.ts +2 -1
  115. package/dist/utils/graphqlError.js +17 -3
  116. package/dist/utils/graphqlError.js.map +1 -1
  117. package/dist/utils/greatFile.d.ts +40 -0
  118. package/dist/utils/greatFile.js +66 -0
  119. package/dist/utils/greatFile.js.map +1 -0
  120. package/dist/utils/hashContent.js +1 -1
  121. package/dist/utils/hashContent.js.map +1 -1
  122. package/dist/utils/integrationType.d.ts +3 -0
  123. package/dist/utils/integrationType.js +15 -0
  124. package/dist/utils/integrationType.js.map +1 -0
  125. package/dist/utils/parseJson.d.ts +1 -0
  126. package/dist/utils/parseJson.js +5 -0
  127. package/dist/utils/parseJson.js.map +1 -0
  128. package/dist/utils/resolveAccount.d.ts +5 -4
  129. package/dist/utils/resolveAccount.js +17 -9
  130. package/dist/utils/resolveAccount.js.map +1 -1
  131. package/dist/utils/resolveElement.d.ts +1 -1
  132. package/dist/utils/resolveElementInclude.d.ts +3 -0
  133. package/dist/utils/resolveElementInclude.js +28 -0
  134. package/dist/utils/resolveElementInclude.js.map +1 -0
  135. package/dist/utils/resolveIntegration.d.ts +4 -2
  136. package/dist/utils/resolveIntegration.js +7 -3
  137. package/dist/utils/resolveIntegration.js.map +1 -1
  138. package/dist/utils/resolveTemplateImport.d.ts +3 -0
  139. package/dist/utils/resolveTemplateImport.js +31 -0
  140. package/dist/utils/resolveTemplateImport.js.map +1 -0
  141. package/dist/utils/response.d.ts +3 -0
  142. package/dist/utils/response.js +24 -0
  143. package/dist/utils/response.js.map +1 -0
  144. package/dist/utils/saveIntegration.d.ts +8 -4
  145. package/dist/utils/saveIntegration.js +76 -34
  146. package/dist/utils/saveIntegration.js.map +1 -1
  147. package/dist/utils/token.js +1 -1
  148. package/dist/utils/token.js.map +1 -1
  149. package/dist/utils/url.d.ts +1 -0
  150. package/dist/utils/url.js +10 -0
  151. package/dist/utils/url.js.map +1 -0
  152. package/dist/utils/writeShadowElement.d.ts +26 -0
  153. package/dist/utils/writeShadowElement.js +119 -0
  154. package/dist/utils/writeShadowElement.js.map +1 -0
  155. package/dist/{value → values}/get.d.ts +1 -1
  156. package/dist/{value → values}/get.js +7 -8
  157. package/dist/values/get.js.map +1 -0
  158. package/dist/{value → values}/index.js +13 -1
  159. package/dist/values/index.js.map +1 -0
  160. package/dist/{value → values}/list.js +10 -9
  161. package/dist/values/list.js.map +1 -0
  162. package/dist/{value → values}/set.d.ts +1 -1
  163. package/dist/{value → values}/set.js +9 -9
  164. package/dist/values/set.js.map +1 -0
  165. package/dist/values/unset.d.ts +8 -0
  166. package/dist/values/unset.js +61 -0
  167. package/dist/values/unset.js.map +1 -0
  168. package/dist/whoami/fetchSession.d.ts +3 -3
  169. package/dist/whoami/fetchSession.js +2 -4
  170. package/dist/whoami/fetchSession.js.map +1 -1
  171. package/dist/whoami/index.d.ts +1 -1
  172. package/package.json +20 -19
  173. package/dist/generated/graphql.d.ts +0 -1784
  174. package/dist/generated/graphql.js.map +0 -1
  175. package/dist/value/get.js.map +0 -1
  176. package/dist/value/index.js.map +0 -1
  177. package/dist/value/list.js.map +0 -1
  178. package/dist/value/set.js.map +0 -1
  179. /package/dist/{value → values}/index.d.ts +0 -0
  180. /package/dist/{value → values}/list.d.ts +0 -0
@@ -1,17 +1,20 @@
1
1
  import fs from 'node:fs';
2
- import path from 'node:path';
3
2
  import chalk from 'chalk';
3
+ import parseJson from '../utils/parseJson.js';
4
4
  import { sentenceCase } from 'change-case';
5
- import { fetchIdFromFile, fetchIntegrationFromFile, } from '../utils/fetchLocal.js';
5
+ import { fetchIntegrationFromFile } from '../utils/fetchLocal.js';
6
+ import { readGreatFile, writeGreatFile, getElementRecord, setElementRecord, removeElementRecordById, sameProvenance, } from '../utils/greatFile.js';
7
+ import { readElementFolder, folderToElementType, } from '../utils/elementFiles.js';
6
8
  import createIntegrationFromFile from '../utils/createIntegrationFromFile.js';
7
- import { createGraphQLClient, } from '../utils/graphqlClient.js';
8
- import { handleGraphQLError } from '../utils/graphqlError.js';
9
- import { getApiBaseUrl } from '../utils/config.js';
9
+ import { getClient } from '../utils/graphqlClient.js';
10
+ import { getGraphQLErrorMessage, handleGraphQLError, } from '../utils/graphqlError.js';
10
11
  import hashContent from '../utils/hashContent.js';
11
12
  import { VALID_VALUE_TYPES, isValidValueElementType, } from '../utils/valueTypes.js';
12
- import { setValueForElement } from '../value/set.js';
13
+ import { setValueForElement } from '../values/set.js';
13
14
  import { promptForValueSetting } from '../utils/promptForValue.js';
14
- import { ElementType, } from '../generated/graphql.js';
15
+ import { SHADOW_MARKER } from '../utils/writeShadowElement.js';
16
+ import resolveIntegrationType from '../utils/integrationType.js';
17
+ import { ElementType } from '../generated/schema.js';
15
18
  import { gql } from 'graphql-tag';
16
19
  const IntegrationForPushQueryDocument = gql `
17
20
  query IntegrationForPush($id: ID!) {
@@ -19,6 +22,14 @@ const IntegrationForPushQueryDocument = gql `
19
22
  id
20
23
  key
21
24
  name
25
+ type
26
+ templateImports {
27
+ id
28
+ timestamp
29
+ template {
30
+ id
31
+ }
32
+ }
22
33
  latest {
23
34
  timestamp
24
35
  elements {
@@ -30,13 +41,23 @@ const IntegrationForPushQueryDocument = gql `
30
41
  createdAt
31
42
  content {
32
43
  checksum
33
- timestamp
44
+ createdAt
45
+ sourceTimestamp
46
+ provenance
34
47
  }
35
48
  }
36
49
  }
37
50
  }
38
51
  }
39
52
  `;
53
+ const UpdateIntegrationTypeMutationDocument = gql `
54
+ mutation UpdateIntegrationType($id: ID!, $type: IntegrationType!) {
55
+ updateIntegrationType(id: $id, type: $type) {
56
+ id
57
+ type
58
+ }
59
+ }
60
+ `;
40
61
  const CreateElementMutationDocument = gql `
41
62
  mutation CreateElement(
42
63
  $integrationId: ID!
@@ -57,6 +78,52 @@ const CreateElementMutationDocument = gql `
57
78
  }
58
79
  }
59
80
  `;
81
+ const IncludeElementMutationDocument = gql `
82
+ mutation IncludeElement(
83
+ $integrationId: ID!
84
+ $elementId: ID!
85
+ $sourceTimestamp: DateTime
86
+ $provenance: [ID!]
87
+ ) {
88
+ includeElement(
89
+ integrationId: $integrationId
90
+ elementId: $elementId
91
+ sourceTimestamp: $sourceTimestamp
92
+ provenance: $provenance
93
+ ) {
94
+ id
95
+ name
96
+ description
97
+ content {
98
+ data
99
+ checksum
100
+ createdAt
101
+ }
102
+ }
103
+ }
104
+ `;
105
+ const RegisterTemplateImportMutationDocument = gql `
106
+ mutation RegisterTemplateImport(
107
+ $integrationId: ID!
108
+ $templateId: ID!
109
+ $timestamp: DateTime!
110
+ ) {
111
+ registerTemplateImport(
112
+ integrationId: $integrationId
113
+ templateId: $templateId
114
+ timestamp: $timestamp
115
+ ) {
116
+ id
117
+ templateImports {
118
+ id
119
+ timestamp
120
+ template {
121
+ id
122
+ }
123
+ }
124
+ }
125
+ }
126
+ `;
60
127
  const UpdateElementMutationDocument = gql `
61
128
  mutation UpdateElement($id: ID!, $name: String, $description: String) {
62
129
  updateElement(id: $id, name: $name, description: $description) {
@@ -71,7 +138,7 @@ const SetElementContentMutationDocument = gql `
71
138
  setElementContent(id: $id, content: $content) {
72
139
  id
73
140
  content {
74
- timestamp
141
+ createdAt
75
142
  checksum
76
143
  }
77
144
  }
@@ -85,38 +152,16 @@ const DeleteElementMutationDocument = gql `
85
152
  }
86
153
  }
87
154
  `;
88
- const folderToElementType = {
89
- schemas: ElementType.Schema,
90
- services: ElementType.Service,
91
- auths: ElementType.Auth,
92
- mutations: ElementType.Mutation,
93
- dictionaries: ElementType.Dictionary,
94
- jobs: ElementType.Job,
95
- values: ElementType.Value,
96
- flows: ElementType.Flow,
97
- retryRules: ElementType.RetryRule,
98
- };
155
+ const SetSecretRefMutationDocument = gql `
156
+ mutation SetSecretRef($id: ID!, $secretRef: String!) {
157
+ setSecretRef(id: $id, secretRef: $secretRef)
158
+ }
159
+ `;
99
160
  function readLocalElements(fsImpl) {
100
161
  const elements = [];
101
162
  for (const folder of Object.keys(folderToElementType)) {
102
- if (!fsImpl.existsSync(folder)) {
103
- continue;
104
- }
105
- const files = fsImpl.readdirSync(folder);
106
- for (const file of files) {
107
- if (!file.endsWith('.json')) {
108
- continue;
109
- }
110
- const key = file.replace('.json', '');
111
- const filePath = path.join(folder, file);
112
- try {
113
- const content = fsImpl.readFileSync(filePath, 'utf8');
114
- const data = JSON.parse(content);
115
- elements.push({ folder, key, filePath, data });
116
- }
117
- catch (error) {
118
- throw new Error(`Failed to read or parse ${filePath}: ${error}`);
119
- }
163
+ for (const { key, filePath, data } of readElementFolder(folder, undefined, fsImpl)) {
164
+ elements.push({ folder, key, filePath, data });
120
165
  }
121
166
  }
122
167
  return elements;
@@ -125,15 +170,32 @@ function compareLocalAndRemote(local, remote) {
125
170
  if (!remote) {
126
171
  return {
127
172
  category: 'conflict',
128
- reason: 'Element exists locally with __id but not found in Integreat',
173
+ reason: 'Element exists locally with an id but not found in Integreat',
129
174
  };
130
175
  }
131
- const localChecksum = hashContent(local.data);
132
176
  const localName = local.data.name || '';
133
177
  const localDesc = local.data.description || '';
134
- const localTimestamp = typeof local.data.__timestamp === 'string'
135
- ? new Date(local.data.__timestamp)
136
- : undefined;
178
+ if (local.source) {
179
+ const hasNameChanged = localName !== (remote.name || '');
180
+ const hasDescriptionChanged = localDesc !== (remote.description || '');
181
+ const hasPinChanged = (local.source.timestamp ?? null) !== (remote.sourceTimestamp ?? null);
182
+ const hasProvenanceChanged = !sameProvenance(local.provenance, remote.provenance);
183
+ if (!hasNameChanged &&
184
+ !hasDescriptionChanged &&
185
+ !hasPinChanged &&
186
+ !hasProvenanceChanged) {
187
+ return { category: 'unchanged' };
188
+ }
189
+ if (hasPinChanged && local.timestamp !== remote.contentCreatedAt) {
190
+ return {
191
+ category: 'conflict',
192
+ reason: ' Shadow pin changed both local and remote',
193
+ };
194
+ }
195
+ return { category: 'update' };
196
+ }
197
+ const localChecksum = hashContent(local.data);
198
+ const localTimestamp = local.timestamp ? new Date(local.timestamp) : undefined;
137
199
  const remoteChecksum = remote.checksum || '';
138
200
  const remoteName = remote.name || '';
139
201
  const remoteDesc = remote.description || '';
@@ -148,7 +210,7 @@ function compareLocalAndRemote(local, remote) {
148
210
  }
149
211
  const metaDateConflict = localTimestamp && remoteCreatedAt && localTimestamp < remoteCreatedAt;
150
212
  const conflicts = [];
151
- if (hasContentChanged && local.data.__timestamp !== remote.timestamp) {
213
+ if (hasContentChanged && local.timestamp !== remote.contentCreatedAt) {
152
214
  conflicts.push('Content changed both local and remote');
153
215
  }
154
216
  if (hasNameChanged && metaDateConflict) {
@@ -169,12 +231,17 @@ function compareLocalAndRemote(local, remote) {
169
231
  }
170
232
  function categorizeElements(localElements, remoteElementsMap) {
171
233
  const toCreate = [];
234
+ const toInclude = [];
172
235
  const toUpdate = [];
173
236
  const toDelete = [];
174
237
  const conflicts = [];
175
238
  const localElementIds = new Set();
176
239
  for (const local of localElements) {
177
- const localId = local.data.__id;
240
+ const localId = local.id;
241
+ if (local.source && !localId) {
242
+ toInclude.push(local);
243
+ continue;
244
+ }
178
245
  if (!localId) {
179
246
  toCreate.push(local);
180
247
  continue;
@@ -194,11 +261,11 @@ function categorizeElements(localElements, remoteElementsMap) {
194
261
  toDelete.push(remoteLement);
195
262
  }
196
263
  }
197
- return { toCreate, toUpdate, toDelete, conflicts };
264
+ return { toCreate, toInclude, toUpdate, toDelete, conflicts };
198
265
  }
199
- const prepareContentForApi = ({ __id, __key, __timestamp, name, description, ...content }, elementType) => {
266
+ const prepareContentForApi = ({ name, description, ...content }, elementType) => {
200
267
  if (elementType === ElementType.Value) {
201
- const { value, type, ...rest } = content;
268
+ const { value, ref, type, ...rest } = content;
202
269
  if (type === undefined) {
203
270
  throw new Error('Value element must have a "type" field. ' +
204
271
  'Valid types: ' +
@@ -233,6 +300,65 @@ async function createElementOnApi(client, integrationId, element) {
233
300
  }
234
301
  return { id, name };
235
302
  }
303
+ function writeShadowFileAndRecord(fsImpl, element, great, source, included) {
304
+ const name = element.data.name || included.name;
305
+ const description = element.data.description ||
306
+ included.description ||
307
+ undefined;
308
+ const content = included.content;
309
+ const fileData = {
310
+ [SHADOW_MARKER]: true,
311
+ ...(name ? { name } : {}),
312
+ ...(description ? { description } : {}),
313
+ ...content?.data,
314
+ };
315
+ fsImpl.writeFileSync(element.filePath, JSON.stringify(fileData, null, 2));
316
+ setElementRecord(great, element.folder, element.key, {
317
+ id: included.id,
318
+ ...(content?.createdAt ? { timestamp: content.createdAt } : {}),
319
+ ...(content?.checksum ? { hash: content.checksum } : {}),
320
+ source,
321
+ ...(element.provenance && element.provenance.length > 0
322
+ ? { provenance: element.provenance }
323
+ : {}),
324
+ });
325
+ }
326
+ async function includeShadowElement(element, graphqlClient, integrationId, fsImpl, log, great) {
327
+ const source = element.source;
328
+ if (!source) {
329
+ return false;
330
+ }
331
+ const provenance = element.provenance && element.provenance.length > 0
332
+ ? element.provenance
333
+ : undefined;
334
+ const result = await graphqlClient.request(IncludeElementMutationDocument, {
335
+ integrationId,
336
+ elementId: source.id,
337
+ sourceTimestamp: source.timestamp,
338
+ provenance,
339
+ });
340
+ if (result.error || !result.data?.includeElement) {
341
+ log(chalk.red(`Failed to include shadow element ${element.key} in ${element.folder}`));
342
+ return false;
343
+ }
344
+ const included = result.data.includeElement;
345
+ const localName = element.data.name || '';
346
+ const localDesc = element.data.description || '';
347
+ const nameDiffers = localName !== (included.name || '');
348
+ const descDiffers = localDesc !== (included.description || '');
349
+ if (nameDiffers || descDiffers) {
350
+ const updateResult = await graphqlClient.request(UpdateElementMutationDocument, {
351
+ id: included.id,
352
+ name: nameDiffers ? localName : undefined,
353
+ description: descDiffers ? localDesc : undefined,
354
+ });
355
+ if (updateResult.error) {
356
+ log(chalk.yellow(`Warning: Failed to update metadata for shadow ${element.key} in ${element.folder}`));
357
+ }
358
+ }
359
+ writeShadowFileAndRecord(fsImpl, element, great, source, included);
360
+ return true;
361
+ }
236
362
  async function updateElementContent(client, elementId, content) {
237
363
  const result = await client.request(SetElementContentMutationDocument, {
238
364
  id: elementId,
@@ -241,25 +367,36 @@ async function updateElementContent(client, elementId, content) {
241
367
  if (result.error) {
242
368
  return undefined;
243
369
  }
244
- return result.data?.setElementContent?.content?.timestamp;
370
+ return result.data?.setElementContent?.content?.createdAt;
245
371
  }
246
- function updateLocalFile(fsImpl, filePath, updatedId, updatedTimestamp, generatedName, keepValue = false) {
247
- const content = fsImpl.readFileSync(filePath, 'utf8');
248
- const parsed = JSON.parse(content);
249
- const { __id, __key, __timestamp, name, description, value, ...data } = parsed;
250
- const id = updatedId || __id;
251
- const timestamp = updatedTimestamp || __timestamp;
372
+ function updateLocalFile(fsImpl, element, great, updatedId, updatedTimestamp, generatedName, keepValue = false) {
373
+ const content = fsImpl.readFileSync(element.filePath, 'utf8');
374
+ const parsed = parseJson(content);
375
+ const { name, description, value, ref, ...data } = parsed;
376
+ const existing = getElementRecord(great, element.folder, element.key);
377
+ const id = updatedId ?? existing?.id;
378
+ const timestamp = updatedTimestamp ?? existing?.timestamp;
252
379
  const finalName = generatedName || name;
253
380
  const orderedData = {
254
- ...(id ? { __id: id } : {}),
255
- __key,
256
- ...(timestamp ? { __timestamp: timestamp } : {}),
257
381
  ...(finalName ? { name: finalName } : {}),
258
382
  ...(description ? { description } : {}),
259
383
  ...data,
260
384
  ...(keepValue && value !== undefined ? { value } : {}),
385
+ ...(keepValue && ref !== undefined ? { ref } : {}),
261
386
  };
262
- fsImpl.writeFileSync(filePath, JSON.stringify(orderedData, null, 2));
387
+ fsImpl.writeFileSync(element.filePath, JSON.stringify(orderedData, null, 2));
388
+ setElementRecord(great, element.folder, element.key, {
389
+ ...(id ? { id } : {}),
390
+ ...(timestamp ? { timestamp } : {}),
391
+ hash: hashContent(orderedData),
392
+ });
393
+ }
394
+ function recordElementProgress(great, element, fields) {
395
+ const existing = getElementRecord(great, element.folder, element.key);
396
+ setElementRecord(great, element.folder, element.key, {
397
+ ...existing,
398
+ ...fields,
399
+ });
263
400
  }
264
401
  function buildRemoteElementsMap(elements) {
265
402
  const remoteElements = new Map();
@@ -267,10 +404,12 @@ function buildRemoteElementsMap(elements) {
267
404
  remoteElements.set(element.id, {
268
405
  id: element.id,
269
406
  checksum: element.content?.checksum,
270
- timestamp: element.content?.timestamp,
407
+ contentCreatedAt: element.content?.createdAt,
271
408
  name: element.name,
272
409
  description: element.description,
273
410
  createdAt: element.createdAt,
411
+ sourceTimestamp: element.content?.sourceTimestamp,
412
+ provenance: element.content?.provenance,
274
413
  });
275
414
  }
276
415
  return remoteElements;
@@ -283,7 +422,7 @@ function handleConflicts(conflicts, log) {
283
422
  }
284
423
  log(`\nRun ${chalk.bold('great pull')} to get remote changes, then manually merge and push again.`);
285
424
  }
286
- async function createOrUpdateElement(element, graphqlClient, integrationId, fsImpl, log, apiElementsMap, shouldSetValue) {
425
+ async function createOrUpdateElement(element, graphqlClient, integrationId, fsImpl, log, apiElementsMap, shouldSetValue, great) {
287
426
  let id = element.remoteId;
288
427
  let generatedName;
289
428
  if (!id) {
@@ -294,6 +433,7 @@ async function createOrUpdateElement(element, graphqlClient, integrationId, fsIm
294
433
  }
295
434
  id = result.id;
296
435
  generatedName = result.name;
436
+ recordElementProgress(great, element, { id });
297
437
  }
298
438
  else {
299
439
  const apiElement = apiElementsMap.get(id);
@@ -316,6 +456,15 @@ async function createOrUpdateElement(element, graphqlClient, integrationId, fsIm
316
456
  }
317
457
  }
318
458
  }
459
+ if (element.source) {
460
+ const remote = apiElementsMap.get(id);
461
+ const pinChanged = (element.source.timestamp ?? null) !== (remote?.sourceTimestamp ?? null);
462
+ const provenanceChanged = !sameProvenance(element.provenance, remote?.provenance);
463
+ if (pinChanged || provenanceChanged) {
464
+ return includeShadowElement(element, graphqlClient, integrationId, fsImpl, log, great);
465
+ }
466
+ return true;
467
+ }
319
468
  const elementType = folderToElementType[element.folder];
320
469
  let content;
321
470
  try {
@@ -330,17 +479,31 @@ async function createOrUpdateElement(element, graphqlClient, integrationId, fsIm
330
479
  log(chalk.red(`Failed to ${element.remoteId ? 'update' : 'set'} content for ${element.key} in ${element.folder}`));
331
480
  return false;
332
481
  }
482
+ recordElementProgress(great, element, { id, timestamp });
333
483
  const hasValue = element.data.value !== undefined;
334
- if (elementType === ElementType.Value && hasValue && shouldSetValue) {
335
- const valueStr = String(element.data.value);
336
- const result = await setValueForElement(id, valueStr, content, graphqlClient);
337
- if (!result.success) {
338
- log(chalk.red(`Failed to set value for ${element.key}: ${result.error}`));
339
- return false;
484
+ const hasRef = element.data.ref !== undefined;
485
+ const isSecret = content.type === 'secret';
486
+ if (elementType === ElementType.Value && shouldSetValue) {
487
+ if (isSecret && hasRef && !hasValue) {
488
+ const secretRef = String(element.data.ref);
489
+ const result = await graphqlClient.request(SetSecretRefMutationDocument, { id, secretRef });
490
+ if (result.error) {
491
+ log(chalk.red(`Failed to set secret ref for ${element.key}: ${getGraphQLErrorMessage(result.error)}`));
492
+ return false;
493
+ }
494
+ log(chalk.green(`✓ Secret ref set for element "${element.key}"`));
495
+ }
496
+ else if (hasValue) {
497
+ const valueStr = String(element.data.value);
498
+ const result = await setValueForElement(id, valueStr, content, graphqlClient);
499
+ if (!result.success) {
500
+ log(chalk.red(`Failed to set value for ${element.key}: ${result.error}`));
501
+ return false;
502
+ }
503
+ log(chalk.green(`✓ Value set for element "${element.key}" (${content.type})`));
340
504
  }
341
- log(chalk.green(`✓ Value set for element "${element.key}" (${content.type})`));
342
505
  }
343
- updateLocalFile(fsImpl, element.filePath, element.remoteId ? undefined : id, timestamp, generatedName, !shouldSetValue && hasValue);
506
+ updateLocalFile(fsImpl, element, great, element.remoteId ? undefined : id, timestamp, generatedName, !shouldSetValue && hasValue);
344
507
  return true;
345
508
  }
346
509
  async function deleteElement(element, graphqlClient, log) {
@@ -353,29 +516,96 @@ async function deleteElement(element, graphqlClient, log) {
353
516
  }
354
517
  return true;
355
518
  }
356
- function logSummary(createdCount, updatedCount, deletedCount, log) {
357
- const total = createdCount + updatedCount + deletedCount;
519
+ function buildRemoteImportsMap(templateImports) {
520
+ const map = new Map();
521
+ for (const templateImport of templateImports) {
522
+ if (templateImport.template) {
523
+ map.set(templateImport.template.id, templateImport);
524
+ }
525
+ }
526
+ return map;
527
+ }
528
+ function findImportsToRegister(great, remoteImports) {
529
+ return (great.imports ?? []).filter((record) => {
530
+ const remote = remoteImports.get(record.templateId);
531
+ return !remote || remote.timestamp !== record.timestamp;
532
+ });
533
+ }
534
+ function rebuildImportRecords(great, templateImports) {
535
+ const records = [];
536
+ for (const templateImport of templateImports) {
537
+ if (templateImport.template) {
538
+ records.push({
539
+ templateId: templateImport.template.id,
540
+ timestamp: templateImport.timestamp,
541
+ id: templateImport.id,
542
+ });
543
+ }
544
+ }
545
+ if (records.length > 0 || great.imports?.length) {
546
+ great.imports = records;
547
+ }
548
+ }
549
+ async function registerTemplateImports(pending, initialRemoteImports, graphqlClient, integrationId, great, log) {
550
+ let latestRemoteImports = initialRemoteImports;
551
+ let registeredCount = 0;
552
+ for (const record of pending) {
553
+ const result = await graphqlClient.request(RegisterTemplateImportMutationDocument, {
554
+ integrationId,
555
+ templateId: record.templateId,
556
+ timestamp: record.timestamp,
557
+ });
558
+ if (result.error || !result.data?.registerTemplateImport) {
559
+ log(chalk.red(`Failed to register template import ${record.templateId}`));
560
+ return undefined;
561
+ }
562
+ latestRemoteImports = result.data.registerTemplateImport.templateImports;
563
+ log(chalk.green(`✓ Registered template import ${record.templateId}`));
564
+ registeredCount++;
565
+ }
566
+ rebuildImportRecords(great, latestRemoteImports);
567
+ return registeredCount;
568
+ }
569
+ async function syncIntegrationType(graphqlClient, integrationId, remoteType, localType, log) {
570
+ if (localType === remoteType) {
571
+ return;
572
+ }
573
+ const result = await graphqlClient.request(UpdateIntegrationTypeMutationDocument, { id: integrationId, type: localType });
574
+ if (result.error) {
575
+ handleGraphQLError(result.error, log);
576
+ return;
577
+ }
578
+ log(`${chalk.green('✓')} Updated integration type to ${chalk.cyan(localType)}`);
579
+ }
580
+ function logSummary(createdCount, includedCount, updatedCount, deletedCount, registeredCount, log) {
581
+ const total = createdCount + includedCount + updatedCount + deletedCount + registeredCount;
358
582
  if (total > 0) {
359
583
  const parts = [];
360
584
  if (createdCount > 0)
361
585
  parts.push(`${createdCount} created`);
586
+ if (includedCount > 0)
587
+ parts.push(`${includedCount} included`);
362
588
  if (updatedCount > 0)
363
589
  parts.push(`${updatedCount} updated`);
364
590
  if (deletedCount > 0)
365
591
  parts.push(`${deletedCount} deleted`);
592
+ if (registeredCount > 0)
593
+ parts.push(`${registeredCount} template imports registered`);
366
594
  log(`\n${chalk.green('✓')} ${parts.join(', ')}`);
367
595
  }
368
596
  }
369
597
  export const action = (log, apiToken, client, fsImpl = fs, config, promptForValueSettingFn = promptForValueSetting) => async function pushIntegration(options) {
370
- let integrationId = fetchIdFromFile(fsImpl);
598
+ const great = readGreatFile(undefined, fsImpl);
599
+ let integrationId = great.id;
371
600
  if (!integrationId) {
372
- log(chalk.yellow('No integration ID found in integration.json'));
601
+ log(chalk.yellow('No integration ID found'));
373
602
  log('Creating integration on server...\n');
374
- const apiBaseUrl = getApiBaseUrl(config);
375
- const graphqlClient = client ?? createGraphQLClient(apiToken, apiBaseUrl);
603
+ const graphqlClient = getClient(apiToken, client, config);
376
604
  const result = await createIntegrationFromFile(graphqlClient, apiToken ?? '', options ?? {}, config, fsImpl);
377
605
  if (!result.success || !result.integration) {
378
- log(chalk.red(result.error));
606
+ if (result.error) {
607
+ handleGraphQLError(result.error, log);
608
+ }
379
609
  return;
380
610
  }
381
611
  const { integration } = result;
@@ -390,27 +620,26 @@ export const action = (log, apiToken, client, fsImpl = fs, config, promptForValu
390
620
  return;
391
621
  }
392
622
  const updatedData = {
393
- __id: integration.id,
394
- __key: integration.key,
395
623
  name: integration.name,
396
624
  description: fileData.description,
397
625
  status: integration.status,
398
- account: {
399
- __id: integration.account.id,
400
- __key: integration.account.key,
401
- name: integration.account.name,
402
- },
626
+ type: integration.type,
403
627
  flags: fileData.flags,
404
628
  identConfig: fileData.identConfig,
405
629
  nonvalues: fileData.nonvalues,
406
630
  queueService: fileData.queueService,
407
631
  };
408
632
  fsImpl.writeFileSync('integration.json', JSON.stringify(updatedData, null, 2));
633
+ great.id = integration.id;
634
+ great.account = {
635
+ id: integration.account.id,
636
+ key: integration.account.key,
637
+ };
638
+ writeGreatFile(great, undefined, fsImpl);
409
639
  integrationId = integration.id;
410
640
  log(chalk.green('✓ Updated integration.json with server details\n'));
411
641
  }
412
- const apiBaseUrl = getApiBaseUrl(config);
413
- const graphqlClient = client ?? createGraphQLClient(apiToken, apiBaseUrl);
642
+ const graphqlClient = getClient(apiToken, client, config);
414
643
  const result = await graphqlClient.request(IntegrationForPushQueryDocument, { id: integrationId });
415
644
  if (result.error) {
416
645
  handleGraphQLError(result.error, log);
@@ -421,6 +650,8 @@ export const action = (log, apiToken, client, fsImpl = fs, config, promptForValu
421
650
  log(chalk.red('Integration has no latest version'));
422
651
  return;
423
652
  }
653
+ const localData = fetchIntegrationFromFile(fsImpl);
654
+ await syncIntegrationType(graphqlClient, integrationId, resolveIntegrationType(integration.type), resolveIntegrationType(localData?.type, log), log);
424
655
  const apiElementsMap = buildRemoteElementsMap(integration.latest.elements);
425
656
  let localElements;
426
657
  try {
@@ -430,12 +661,26 @@ export const action = (log, apiToken, client, fsImpl = fs, config, promptForValu
430
661
  log(chalk.red(`Error reading local files: ${error}`));
431
662
  return;
432
663
  }
433
- const { toCreate, toUpdate, toDelete, conflicts } = categorizeElements(localElements, apiElementsMap);
664
+ for (const local of localElements) {
665
+ const record = getElementRecord(great, local.folder, local.key);
666
+ local.id = record?.id;
667
+ local.timestamp = record?.timestamp;
668
+ local.source = record?.source;
669
+ local.provenance = record?.provenance;
670
+ }
671
+ const { toCreate, toInclude, toUpdate, toDelete, conflicts } = categorizeElements(localElements, apiElementsMap);
434
672
  if (conflicts.length > 0) {
435
673
  handleConflicts(conflicts, log);
436
674
  return;
437
675
  }
438
- if (toCreate.length + toUpdate.length + toDelete.length === 0) {
676
+ const remoteImportsMap = buildRemoteImportsMap(integration.templateImports ?? []);
677
+ const importsToRegister = findImportsToRegister(great, remoteImportsMap);
678
+ if (toCreate.length +
679
+ toInclude.length +
680
+ toUpdate.length +
681
+ toDelete.length +
682
+ importsToRegister.length ===
683
+ 0) {
439
684
  log(chalk.green('No changes to push'));
440
685
  return;
441
686
  }
@@ -447,31 +692,57 @@ export const action = (log, apiToken, client, fsImpl = fs, config, promptForValu
447
692
  log(chalk.yellow('\nPush cancelled'));
448
693
  return;
449
694
  }
695
+ const persistProgress = () => writeGreatFile(great, undefined, fsImpl);
450
696
  let createdCount = 0;
451
697
  for (const element of toCreate) {
452
- const success = await createOrUpdateElement(element, graphqlClient, integrationId, fsImpl, log, apiElementsMap, shouldSetValue);
453
- if (!success)
698
+ const success = await createOrUpdateElement(element, graphqlClient, integrationId, fsImpl, log, apiElementsMap, shouldSetValue, great);
699
+ if (!success) {
700
+ persistProgress();
454
701
  return;
702
+ }
455
703
  log(chalk.green(`✓ Created ${element.folder}/${element.key}`));
456
704
  createdCount++;
457
705
  }
706
+ let includedCount = 0;
707
+ for (const element of toInclude) {
708
+ const success = await includeShadowElement(element, graphqlClient, integrationId, fsImpl, log, great);
709
+ if (!success) {
710
+ persistProgress();
711
+ return;
712
+ }
713
+ log(chalk.green(`✓ Included ${element.folder}/${element.key}`));
714
+ includedCount++;
715
+ }
458
716
  let updatedCount = 0;
459
717
  for (const element of toUpdate) {
460
- const success = await createOrUpdateElement(element, graphqlClient, integrationId, fsImpl, log, apiElementsMap, shouldSetValue);
461
- if (!success)
718
+ const success = await createOrUpdateElement(element, graphqlClient, integrationId, fsImpl, log, apiElementsMap, shouldSetValue, great);
719
+ if (!success) {
720
+ persistProgress();
462
721
  return;
722
+ }
463
723
  log(chalk.green(`✓ Updated ${element.folder}/${element.key}`));
464
724
  updatedCount++;
465
725
  }
466
726
  let deletedCount = 0;
467
727
  for (const element of toDelete) {
468
728
  const success = await deleteElement(element, graphqlClient, log);
469
- if (!success)
729
+ if (!success) {
730
+ persistProgress();
470
731
  return;
732
+ }
733
+ removeElementRecordById(great, element.id);
471
734
  log(chalk.green(`✓ Deleted ${element.id}`));
472
735
  deletedCount++;
473
736
  }
474
- logSummary(createdCount, updatedCount, deletedCount, log);
737
+ const registeredCount = await registerTemplateImports(importsToRegister, integration.templateImports ?? [], graphqlClient, integrationId, great, log);
738
+ if (registeredCount === undefined) {
739
+ persistProgress();
740
+ return;
741
+ }
742
+ const localKeys = new Set(localElements.map((el) => `${el.folder}/${el.key}`));
743
+ great.elements = Object.fromEntries(Object.entries(great.elements).filter(([key]) => localKeys.has(key)));
744
+ writeGreatFile(great, undefined, fsImpl);
745
+ logSummary(createdCount, includedCount, updatedCount, deletedCount, registeredCount, log);
475
746
  };
476
747
  export default (program, apiToken) => program
477
748
  .command('push')