great 1.1.0 → 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 (215) hide show
  1. package/README.md +137 -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 +54 -14
  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 +3 -1
  21. package/dist/explode/transform.js.map +1 -1
  22. package/dist/generated/operations.d.ts +617 -0
  23. package/dist/generated/operations.js +2 -0
  24. package/dist/generated/operations.js.map +1 -0
  25. package/dist/generated/schema.d.ts +965 -0
  26. package/dist/generated/{graphql.js → schema.js} +13 -1
  27. package/dist/generated/schema.js.map +1 -0
  28. package/dist/implode/index.js +22 -43
  29. package/dist/implode/index.js.map +1 -1
  30. package/dist/implode/transform.d.ts +3 -5
  31. package/dist/implode/transform.js +10 -10
  32. package/dist/implode/transform.js.map +1 -1
  33. package/dist/{integrations/describe.d.ts → import/index.d.ts} +4 -1
  34. package/dist/import/index.js +43 -0
  35. package/dist/import/index.js.map +1 -0
  36. package/dist/include/index.d.ts +10 -0
  37. package/dist/include/index.js +41 -0
  38. package/dist/include/index.js.map +1 -0
  39. package/dist/index.js +13 -2
  40. package/dist/index.js.map +1 -1
  41. package/dist/init/index.js +8 -16
  42. package/dist/init/index.js.map +1 -1
  43. package/dist/integrations/create.d.ts +1 -1
  44. package/dist/integrations/create.js +2 -4
  45. package/dist/integrations/create.js.map +1 -1
  46. package/dist/integrations/list.d.ts +1 -1
  47. package/dist/integrations/list.js +2 -4
  48. package/dist/integrations/list.js.map +1 -1
  49. package/dist/migrate/index.d.ts +5 -0
  50. package/dist/migrate/index.js +101 -0
  51. package/dist/migrate/index.js.map +1 -0
  52. package/dist/pull/index.d.ts +4 -2
  53. package/dist/pull/index.js +25 -44
  54. package/dist/pull/index.js.map +1 -1
  55. package/dist/push/index.d.ts +1 -1
  56. package/dist/push/index.js +333 -96
  57. package/dist/push/index.js.map +1 -1
  58. package/dist/rename/index.d.ts +8 -0
  59. package/dist/rename/index.js +109 -0
  60. package/dist/rename/index.js.map +1 -0
  61. package/dist/repl/dispatch.d.ts +8 -2
  62. package/dist/repl/dispatch.js +43 -29
  63. package/dist/repl/dispatch.js.map +1 -1
  64. package/dist/repl/dispatchAndLoad.d.ts +7 -0
  65. package/dist/repl/dispatchAndLoad.js +35 -0
  66. package/dist/repl/dispatchAndLoad.js.map +1 -0
  67. package/dist/repl/help.js +8 -1
  68. package/dist/repl/help.js.map +1 -1
  69. package/dist/repl/index.d.ts +1 -1
  70. package/dist/repl/index.js +29 -9
  71. package/dist/repl/index.js.map +1 -1
  72. package/dist/repl/load.d.ts +8 -0
  73. package/dist/repl/load.js +53 -0
  74. package/dist/repl/load.js.map +1 -0
  75. package/dist/run/index.d.ts +4 -5
  76. package/dist/run/index.js +4 -6
  77. package/dist/run/index.js.map +1 -1
  78. package/dist/status/index.d.ts +1 -1
  79. package/dist/status/index.js +5 -7
  80. package/dist/status/index.js.map +1 -1
  81. package/dist/utils/config.d.ts +9 -2
  82. package/dist/utils/config.js +39 -6
  83. package/dist/utils/config.js.map +1 -1
  84. package/dist/utils/createIntegrationFromFile.d.ts +2 -0
  85. package/dist/utils/createIntegrationFromFile.js +14 -33
  86. package/dist/utils/createIntegrationFromFile.js.map +1 -1
  87. package/dist/utils/detectPullConflicts.d.ts +8 -0
  88. package/dist/utils/detectPullConflicts.js +65 -0
  89. package/dist/utils/detectPullConflicts.js.map +1 -0
  90. package/dist/utils/dispatch.d.ts +1 -1
  91. package/dist/utils/dispatch.js +3 -2
  92. package/dist/utils/dispatch.js.map +1 -1
  93. package/dist/utils/elementFiles.d.ts +13 -0
  94. package/dist/utils/elementFiles.js +76 -0
  95. package/dist/utils/elementFiles.js.map +1 -0
  96. package/dist/utils/fetchIntegrationDefinition.d.ts +8 -0
  97. package/dist/utils/fetchIntegrationDefinition.js +58 -0
  98. package/dist/utils/fetchIntegrationDefinition.js.map +1 -0
  99. package/dist/utils/fetchLocal.d.ts +3 -1
  100. package/dist/utils/fetchLocal.js +11 -9
  101. package/dist/utils/fetchLocal.js.map +1 -1
  102. package/dist/utils/fetchSession.d.ts +1 -1
  103. package/dist/utils/fetchSession.js +2 -1
  104. package/dist/utils/fetchSession.js.map +1 -1
  105. package/dist/utils/graphqlClient.d.ts +3 -0
  106. package/dist/utils/graphqlClient.js +3 -0
  107. package/dist/utils/graphqlClient.js.map +1 -1
  108. package/dist/utils/graphqlError.d.ts +1 -0
  109. package/dist/utils/graphqlError.js +13 -1
  110. package/dist/utils/graphqlError.js.map +1 -1
  111. package/dist/utils/greatFile.d.ts +40 -0
  112. package/dist/utils/greatFile.js +66 -0
  113. package/dist/utils/greatFile.js.map +1 -0
  114. package/dist/utils/hashContent.js +1 -1
  115. package/dist/utils/hashContent.js.map +1 -1
  116. package/dist/utils/integrationType.d.ts +3 -0
  117. package/dist/utils/integrationType.js +15 -0
  118. package/dist/utils/integrationType.js.map +1 -0
  119. package/dist/utils/resolveAccount.d.ts +3 -2
  120. package/dist/utils/resolveAccount.js +11 -1
  121. package/dist/utils/resolveAccount.js.map +1 -1
  122. package/dist/utils/resolveElement.d.ts +1 -1
  123. package/dist/utils/resolveElementInclude.d.ts +3 -0
  124. package/dist/utils/resolveElementInclude.js +28 -0
  125. package/dist/utils/resolveElementInclude.js.map +1 -0
  126. package/dist/utils/resolveIntegration.d.ts +4 -2
  127. package/dist/utils/resolveIntegration.js +7 -3
  128. package/dist/utils/resolveIntegration.js.map +1 -1
  129. package/dist/utils/resolveTemplateImport.d.ts +3 -0
  130. package/dist/utils/resolveTemplateImport.js +31 -0
  131. package/dist/utils/resolveTemplateImport.js.map +1 -0
  132. package/dist/utils/response.d.ts +3 -0
  133. package/dist/utils/response.js +24 -0
  134. package/dist/utils/response.js.map +1 -0
  135. package/dist/utils/saveIntegration.d.ts +8 -4
  136. package/dist/utils/saveIntegration.js +76 -34
  137. package/dist/utils/saveIntegration.js.map +1 -1
  138. package/dist/utils/token.js +1 -1
  139. package/dist/utils/token.js.map +1 -1
  140. package/dist/utils/url.d.ts +1 -0
  141. package/dist/utils/url.js +10 -0
  142. package/dist/utils/url.js.map +1 -0
  143. package/dist/utils/writeShadowElement.d.ts +26 -0
  144. package/dist/utils/writeShadowElement.js +119 -0
  145. package/dist/utils/writeShadowElement.js.map +1 -0
  146. package/dist/{value → values}/get.d.ts +1 -1
  147. package/dist/{value → values}/get.js +7 -8
  148. package/dist/values/get.js.map +1 -0
  149. package/dist/{value → values}/index.js +13 -1
  150. package/dist/values/index.js.map +1 -0
  151. package/dist/{value → values}/list.js +10 -9
  152. package/dist/values/list.js.map +1 -0
  153. package/dist/{value → values}/set.d.ts +1 -1
  154. package/dist/{value → values}/set.js +9 -9
  155. package/dist/values/set.js.map +1 -0
  156. package/dist/values/unset.d.ts +8 -0
  157. package/dist/values/unset.js +61 -0
  158. package/dist/values/unset.js.map +1 -0
  159. package/dist/whoami/fetchSession.d.ts +3 -3
  160. package/dist/whoami/fetchSession.js +2 -4
  161. package/dist/whoami/fetchSession.js.map +1 -1
  162. package/dist/whoami/index.d.ts +1 -1
  163. package/package.json +14 -14
  164. package/dist/create/account.d.ts +0 -2
  165. package/dist/create/account.js +0 -40
  166. package/dist/create/account.js.map +0 -1
  167. package/dist/create/integration.d.ts +0 -4
  168. package/dist/create/integration.js +0 -73
  169. package/dist/create/integration.js.map +0 -1
  170. package/dist/create/utils.d.ts +0 -4
  171. package/dist/create/utils.js +0 -8
  172. package/dist/create/utils.js.map +0 -1
  173. package/dist/generated/graphql.d.ts +0 -1867
  174. package/dist/generated/graphql.js.map +0 -1
  175. package/dist/integrations/describe.js +0 -51
  176. package/dist/integrations/describe.js.map +0 -1
  177. package/dist/projects/describe.d.ts +0 -5
  178. package/dist/projects/describe.js +0 -51
  179. package/dist/projects/describe.js.map +0 -1
  180. package/dist/projects/index.d.ts +0 -5
  181. package/dist/projects/index.js +0 -83
  182. package/dist/projects/index.js.map +0 -1
  183. package/dist/repl/fetchUserAndIntegration.d.ts +0 -10
  184. package/dist/repl/fetchUserAndIntegration.js +0 -73
  185. package/dist/repl/fetchUserAndIntegration.js.map +0 -1
  186. package/dist/utils/apiUrl.d.ts +0 -1
  187. package/dist/utils/apiUrl.js +0 -7
  188. package/dist/utils/apiUrl.js.map +0 -1
  189. package/dist/utils/callApi.d.ts +0 -5
  190. package/dist/utils/callApi.js +0 -17
  191. package/dist/utils/callApi.js.map +0 -1
  192. package/dist/utils/fetchAccount.d.ts +0 -10
  193. package/dist/utils/fetchAccount.js +0 -60
  194. package/dist/utils/fetchAccount.js.map +0 -1
  195. package/dist/utils/fetchLocalProject.d.ts +0 -1
  196. package/dist/utils/fetchLocalProject.js +0 -12
  197. package/dist/utils/fetchLocalProject.js.map +0 -1
  198. package/dist/utils/fetchUserAndIntegration.d.ts +0 -9
  199. package/dist/utils/fetchUserAndIntegration.js +0 -48
  200. package/dist/utils/fetchUserAndIntegration.js.map +0 -1
  201. package/dist/utils/graphqlRequestClient.d.ts +0 -10
  202. package/dist/utils/graphqlRequestClient.js +0 -20
  203. package/dist/utils/graphqlRequestClient.js.map +0 -1
  204. package/dist/utils/saveProject.d.ts +0 -8
  205. package/dist/utils/saveProject.js +0 -93
  206. package/dist/utils/saveProject.js.map +0 -1
  207. package/dist/utils/session.d.ts +0 -9
  208. package/dist/utils/session.js +0 -17
  209. package/dist/utils/session.js.map +0 -1
  210. package/dist/value/get.js.map +0 -1
  211. package/dist/value/index.js.map +0 -1
  212. package/dist/value/list.js.map +0 -1
  213. package/dist/value/set.js.map +0 -1
  214. /package/dist/{value → values}/index.d.ts +0 -0
  215. /package/dist/{value → values}/list.d.ts +0 -0
@@ -1,18 +1,20 @@
1
1
  import fs from 'node:fs';
2
- import path from 'node:path';
3
2
  import chalk from 'chalk';
4
3
  import parseJson from '../utils/parseJson.js';
5
4
  import { sentenceCase } from 'change-case';
6
- 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';
7
8
  import createIntegrationFromFile from '../utils/createIntegrationFromFile.js';
8
- import { createGraphQLClient, } from '../utils/graphqlClient.js';
9
- import { handleGraphQLError } from '../utils/graphqlError.js';
10
- import { getApiBaseUrl } from '../utils/config.js';
9
+ import { getClient } from '../utils/graphqlClient.js';
10
+ import { getGraphQLErrorMessage, handleGraphQLError, } from '../utils/graphqlError.js';
11
11
  import hashContent from '../utils/hashContent.js';
12
12
  import { VALID_VALUE_TYPES, isValidValueElementType, } from '../utils/valueTypes.js';
13
- import { setValueForElement } from '../value/set.js';
13
+ import { setValueForElement } from '../values/set.js';
14
14
  import { promptForValueSetting } from '../utils/promptForValue.js';
15
- 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';
16
18
  import { gql } from 'graphql-tag';
17
19
  const IntegrationForPushQueryDocument = gql `
18
20
  query IntegrationForPush($id: ID!) {
@@ -20,6 +22,14 @@ const IntegrationForPushQueryDocument = gql `
20
22
  id
21
23
  key
22
24
  name
25
+ type
26
+ templateImports {
27
+ id
28
+ timestamp
29
+ template {
30
+ id
31
+ }
32
+ }
23
33
  latest {
24
34
  timestamp
25
35
  elements {
@@ -31,13 +41,23 @@ const IntegrationForPushQueryDocument = gql `
31
41
  createdAt
32
42
  content {
33
43
  checksum
34
- timestamp
44
+ createdAt
45
+ sourceTimestamp
46
+ provenance
35
47
  }
36
48
  }
37
49
  }
38
50
  }
39
51
  }
40
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
+ `;
41
61
  const CreateElementMutationDocument = gql `
42
62
  mutation CreateElement(
43
63
  $integrationId: ID!
@@ -58,6 +78,52 @@ const CreateElementMutationDocument = gql `
58
78
  }
59
79
  }
60
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
+ `;
61
127
  const UpdateElementMutationDocument = gql `
62
128
  mutation UpdateElement($id: ID!, $name: String, $description: String) {
63
129
  updateElement(id: $id, name: $name, description: $description) {
@@ -72,7 +138,7 @@ const SetElementContentMutationDocument = gql `
72
138
  setElementContent(id: $id, content: $content) {
73
139
  id
74
140
  content {
75
- timestamp
141
+ createdAt
76
142
  checksum
77
143
  }
78
144
  }
@@ -91,50 +157,11 @@ const SetSecretRefMutationDocument = gql `
91
157
  setSecretRef(id: $id, secretRef: $secretRef)
92
158
  }
93
159
  `;
94
- const JSON_EXTENSIONS = ['.jsonc', '.json'];
95
- function elementKeyFromFileName(fileName) {
96
- const ext = JSON_EXTENSIONS.find((e) => fileName.endsWith(e));
97
- return ext ? fileName.slice(0, -ext.length) : undefined;
98
- }
99
- const folderToElementType = {
100
- schemas: ElementType.Schema,
101
- services: ElementType.Service,
102
- auths: ElementType.Auth,
103
- mutations: ElementType.Mutation,
104
- dictionaries: ElementType.Dictionary,
105
- jobs: ElementType.Job,
106
- values: ElementType.Value,
107
- flows: ElementType.Flow,
108
- retryRules: ElementType.RetryRule,
109
- };
110
160
  function readLocalElements(fsImpl) {
111
161
  const elements = [];
112
162
  for (const folder of Object.keys(folderToElementType)) {
113
- if (!fsImpl.existsSync(folder)) {
114
- continue;
115
- }
116
- const files = fsImpl.readdirSync(folder);
117
- const seenKeys = new Map();
118
- for (const file of files) {
119
- const fileName = String(file);
120
- const key = elementKeyFromFileName(fileName);
121
- if (!key) {
122
- continue;
123
- }
124
- const filePath = path.join(folder, fileName);
125
- const existingFile = seenKeys.get(key);
126
- if (existingFile) {
127
- throw new Error(`Duplicate element key "${key}" in ${folder}/: found both ${existingFile} and ${fileName}`);
128
- }
129
- seenKeys.set(key, fileName);
130
- try {
131
- const content = fsImpl.readFileSync(filePath, 'utf8');
132
- const data = parseJson(content);
133
- elements.push({ folder, key, filePath, data });
134
- }
135
- catch (error) {
136
- throw new Error(`Failed to read or parse ${filePath}: ${error}`);
137
- }
163
+ for (const { key, filePath, data } of readElementFolder(folder, undefined, fsImpl)) {
164
+ elements.push({ folder, key, filePath, data });
138
165
  }
139
166
  }
140
167
  return elements;
@@ -143,15 +170,32 @@ function compareLocalAndRemote(local, remote) {
143
170
  if (!remote) {
144
171
  return {
145
172
  category: 'conflict',
146
- reason: 'Element exists locally with __id but not found in Integreat',
173
+ reason: 'Element exists locally with an id but not found in Integreat',
147
174
  };
148
175
  }
149
- const localChecksum = hashContent(local.data);
150
176
  const localName = local.data.name || '';
151
177
  const localDesc = local.data.description || '';
152
- const localTimestamp = typeof local.data.__timestamp === 'string'
153
- ? new Date(local.data.__timestamp)
154
- : 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;
155
199
  const remoteChecksum = remote.checksum || '';
156
200
  const remoteName = remote.name || '';
157
201
  const remoteDesc = remote.description || '';
@@ -166,7 +210,7 @@ function compareLocalAndRemote(local, remote) {
166
210
  }
167
211
  const metaDateConflict = localTimestamp && remoteCreatedAt && localTimestamp < remoteCreatedAt;
168
212
  const conflicts = [];
169
- if (hasContentChanged && local.data.__timestamp !== remote.timestamp) {
213
+ if (hasContentChanged && local.timestamp !== remote.contentCreatedAt) {
170
214
  conflicts.push('Content changed both local and remote');
171
215
  }
172
216
  if (hasNameChanged && metaDateConflict) {
@@ -187,12 +231,17 @@ function compareLocalAndRemote(local, remote) {
187
231
  }
188
232
  function categorizeElements(localElements, remoteElementsMap) {
189
233
  const toCreate = [];
234
+ const toInclude = [];
190
235
  const toUpdate = [];
191
236
  const toDelete = [];
192
237
  const conflicts = [];
193
238
  const localElementIds = new Set();
194
239
  for (const local of localElements) {
195
- const localId = local.data.__id;
240
+ const localId = local.id;
241
+ if (local.source && !localId) {
242
+ toInclude.push(local);
243
+ continue;
244
+ }
196
245
  if (!localId) {
197
246
  toCreate.push(local);
198
247
  continue;
@@ -212,9 +261,9 @@ function categorizeElements(localElements, remoteElementsMap) {
212
261
  toDelete.push(remoteLement);
213
262
  }
214
263
  }
215
- return { toCreate, toUpdate, toDelete, conflicts };
264
+ return { toCreate, toInclude, toUpdate, toDelete, conflicts };
216
265
  }
217
- const prepareContentForApi = ({ __id, __key, __timestamp, name, description, ...content }, elementType) => {
266
+ const prepareContentForApi = ({ name, description, ...content }, elementType) => {
218
267
  if (elementType === ElementType.Value) {
219
268
  const { value, ref, type, ...rest } = content;
220
269
  if (type === undefined) {
@@ -251,6 +300,65 @@ async function createElementOnApi(client, integrationId, element) {
251
300
  }
252
301
  return { id, name };
253
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
+ }
254
362
  async function updateElementContent(client, elementId, content) {
255
363
  const result = await client.request(SetElementContentMutationDocument, {
256
364
  id: elementId,
@@ -259,26 +367,36 @@ async function updateElementContent(client, elementId, content) {
259
367
  if (result.error) {
260
368
  return undefined;
261
369
  }
262
- return result.data?.setElementContent?.content?.timestamp;
370
+ return result.data?.setElementContent?.content?.createdAt;
263
371
  }
264
- function updateLocalFile(fsImpl, filePath, updatedId, updatedTimestamp, generatedName, keepValue = false) {
265
- const content = fsImpl.readFileSync(filePath, 'utf8');
372
+ function updateLocalFile(fsImpl, element, great, updatedId, updatedTimestamp, generatedName, keepValue = false) {
373
+ const content = fsImpl.readFileSync(element.filePath, 'utf8');
266
374
  const parsed = parseJson(content);
267
- const { __id, __key, __timestamp, name, description, value, ref, ...data } = parsed;
268
- const id = updatedId || __id;
269
- const timestamp = updatedTimestamp || __timestamp;
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;
270
379
  const finalName = generatedName || name;
271
380
  const orderedData = {
272
- ...(id ? { __id: id } : {}),
273
- __key,
274
- ...(timestamp ? { __timestamp: timestamp } : {}),
275
381
  ...(finalName ? { name: finalName } : {}),
276
382
  ...(description ? { description } : {}),
277
383
  ...data,
278
384
  ...(keepValue && value !== undefined ? { value } : {}),
279
385
  ...(keepValue && ref !== undefined ? { ref } : {}),
280
386
  };
281
- 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
+ });
282
400
  }
283
401
  function buildRemoteElementsMap(elements) {
284
402
  const remoteElements = new Map();
@@ -286,10 +404,12 @@ function buildRemoteElementsMap(elements) {
286
404
  remoteElements.set(element.id, {
287
405
  id: element.id,
288
406
  checksum: element.content?.checksum,
289
- timestamp: element.content?.timestamp,
407
+ contentCreatedAt: element.content?.createdAt,
290
408
  name: element.name,
291
409
  description: element.description,
292
410
  createdAt: element.createdAt,
411
+ sourceTimestamp: element.content?.sourceTimestamp,
412
+ provenance: element.content?.provenance,
293
413
  });
294
414
  }
295
415
  return remoteElements;
@@ -302,7 +422,7 @@ function handleConflicts(conflicts, log) {
302
422
  }
303
423
  log(`\nRun ${chalk.bold('great pull')} to get remote changes, then manually merge and push again.`);
304
424
  }
305
- async function createOrUpdateElement(element, graphqlClient, integrationId, fsImpl, log, apiElementsMap, shouldSetValue) {
425
+ async function createOrUpdateElement(element, graphqlClient, integrationId, fsImpl, log, apiElementsMap, shouldSetValue, great) {
306
426
  let id = element.remoteId;
307
427
  let generatedName;
308
428
  if (!id) {
@@ -313,6 +433,7 @@ async function createOrUpdateElement(element, graphqlClient, integrationId, fsIm
313
433
  }
314
434
  id = result.id;
315
435
  generatedName = result.name;
436
+ recordElementProgress(great, element, { id });
316
437
  }
317
438
  else {
318
439
  const apiElement = apiElementsMap.get(id);
@@ -335,6 +456,15 @@ async function createOrUpdateElement(element, graphqlClient, integrationId, fsIm
335
456
  }
336
457
  }
337
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
+ }
338
468
  const elementType = folderToElementType[element.folder];
339
469
  let content;
340
470
  try {
@@ -349,6 +479,7 @@ async function createOrUpdateElement(element, graphqlClient, integrationId, fsIm
349
479
  log(chalk.red(`Failed to ${element.remoteId ? 'update' : 'set'} content for ${element.key} in ${element.folder}`));
350
480
  return false;
351
481
  }
482
+ recordElementProgress(great, element, { id, timestamp });
352
483
  const hasValue = element.data.value !== undefined;
353
484
  const hasRef = element.data.ref !== undefined;
354
485
  const isSecret = content.type === 'secret';
@@ -357,7 +488,7 @@ async function createOrUpdateElement(element, graphqlClient, integrationId, fsIm
357
488
  const secretRef = String(element.data.ref);
358
489
  const result = await graphqlClient.request(SetSecretRefMutationDocument, { id, secretRef });
359
490
  if (result.error) {
360
- log(chalk.red(`Failed to set secret ref for ${element.key}: ${result.error.message}`));
491
+ log(chalk.red(`Failed to set secret ref for ${element.key}: ${getGraphQLErrorMessage(result.error)}`));
361
492
  return false;
362
493
  }
363
494
  log(chalk.green(`✓ Secret ref set for element "${element.key}"`));
@@ -372,7 +503,7 @@ async function createOrUpdateElement(element, graphqlClient, integrationId, fsIm
372
503
  log(chalk.green(`✓ Value set for element "${element.key}" (${content.type})`));
373
504
  }
374
505
  }
375
- 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);
376
507
  return true;
377
508
  }
378
509
  async function deleteElement(element, graphqlClient, log) {
@@ -385,26 +516,91 @@ async function deleteElement(element, graphqlClient, log) {
385
516
  }
386
517
  return true;
387
518
  }
388
- function logSummary(createdCount, updatedCount, deletedCount, log) {
389
- 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;
390
582
  if (total > 0) {
391
583
  const parts = [];
392
584
  if (createdCount > 0)
393
585
  parts.push(`${createdCount} created`);
586
+ if (includedCount > 0)
587
+ parts.push(`${includedCount} included`);
394
588
  if (updatedCount > 0)
395
589
  parts.push(`${updatedCount} updated`);
396
590
  if (deletedCount > 0)
397
591
  parts.push(`${deletedCount} deleted`);
592
+ if (registeredCount > 0)
593
+ parts.push(`${registeredCount} template imports registered`);
398
594
  log(`\n${chalk.green('✓')} ${parts.join(', ')}`);
399
595
  }
400
596
  }
401
597
  export const action = (log, apiToken, client, fsImpl = fs, config, promptForValueSettingFn = promptForValueSetting) => async function pushIntegration(options) {
402
- let integrationId = fetchIdFromFile(fsImpl);
598
+ const great = readGreatFile(undefined, fsImpl);
599
+ let integrationId = great.id;
403
600
  if (!integrationId) {
404
- log(chalk.yellow('No integration ID found in integration.json'));
601
+ log(chalk.yellow('No integration ID found'));
405
602
  log('Creating integration on server...\n');
406
- const apiBaseUrl = getApiBaseUrl(config);
407
- const graphqlClient = client ?? createGraphQLClient(apiToken, apiBaseUrl);
603
+ const graphqlClient = getClient(apiToken, client, config);
408
604
  const result = await createIntegrationFromFile(graphqlClient, apiToken ?? '', options ?? {}, config, fsImpl);
409
605
  if (!result.success || !result.integration) {
410
606
  if (result.error) {
@@ -424,27 +620,26 @@ export const action = (log, apiToken, client, fsImpl = fs, config, promptForValu
424
620
  return;
425
621
  }
426
622
  const updatedData = {
427
- __id: integration.id,
428
- __key: integration.key,
429
623
  name: integration.name,
430
624
  description: fileData.description,
431
625
  status: integration.status,
432
- account: {
433
- __id: integration.account.id,
434
- __key: integration.account.key,
435
- name: integration.account.name,
436
- },
626
+ type: integration.type,
437
627
  flags: fileData.flags,
438
628
  identConfig: fileData.identConfig,
439
629
  nonvalues: fileData.nonvalues,
440
630
  queueService: fileData.queueService,
441
631
  };
442
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);
443
639
  integrationId = integration.id;
444
640
  log(chalk.green('✓ Updated integration.json with server details\n'));
445
641
  }
446
- const apiBaseUrl = getApiBaseUrl(config);
447
- const graphqlClient = client ?? createGraphQLClient(apiToken, apiBaseUrl);
642
+ const graphqlClient = getClient(apiToken, client, config);
448
643
  const result = await graphqlClient.request(IntegrationForPushQueryDocument, { id: integrationId });
449
644
  if (result.error) {
450
645
  handleGraphQLError(result.error, log);
@@ -455,6 +650,8 @@ export const action = (log, apiToken, client, fsImpl = fs, config, promptForValu
455
650
  log(chalk.red('Integration has no latest version'));
456
651
  return;
457
652
  }
653
+ const localData = fetchIntegrationFromFile(fsImpl);
654
+ await syncIntegrationType(graphqlClient, integrationId, resolveIntegrationType(integration.type), resolveIntegrationType(localData?.type, log), log);
458
655
  const apiElementsMap = buildRemoteElementsMap(integration.latest.elements);
459
656
  let localElements;
460
657
  try {
@@ -464,12 +661,26 @@ export const action = (log, apiToken, client, fsImpl = fs, config, promptForValu
464
661
  log(chalk.red(`Error reading local files: ${error}`));
465
662
  return;
466
663
  }
467
- 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);
468
672
  if (conflicts.length > 0) {
469
673
  handleConflicts(conflicts, log);
470
674
  return;
471
675
  }
472
- 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) {
473
684
  log(chalk.green('No changes to push'));
474
685
  return;
475
686
  }
@@ -481,31 +692,57 @@ export const action = (log, apiToken, client, fsImpl = fs, config, promptForValu
481
692
  log(chalk.yellow('\nPush cancelled'));
482
693
  return;
483
694
  }
695
+ const persistProgress = () => writeGreatFile(great, undefined, fsImpl);
484
696
  let createdCount = 0;
485
697
  for (const element of toCreate) {
486
- const success = await createOrUpdateElement(element, graphqlClient, integrationId, fsImpl, log, apiElementsMap, shouldSetValue);
487
- if (!success)
698
+ const success = await createOrUpdateElement(element, graphqlClient, integrationId, fsImpl, log, apiElementsMap, shouldSetValue, great);
699
+ if (!success) {
700
+ persistProgress();
488
701
  return;
702
+ }
489
703
  log(chalk.green(`✓ Created ${element.folder}/${element.key}`));
490
704
  createdCount++;
491
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
+ }
492
716
  let updatedCount = 0;
493
717
  for (const element of toUpdate) {
494
- const success = await createOrUpdateElement(element, graphqlClient, integrationId, fsImpl, log, apiElementsMap, shouldSetValue);
495
- if (!success)
718
+ const success = await createOrUpdateElement(element, graphqlClient, integrationId, fsImpl, log, apiElementsMap, shouldSetValue, great);
719
+ if (!success) {
720
+ persistProgress();
496
721
  return;
722
+ }
497
723
  log(chalk.green(`✓ Updated ${element.folder}/${element.key}`));
498
724
  updatedCount++;
499
725
  }
500
726
  let deletedCount = 0;
501
727
  for (const element of toDelete) {
502
728
  const success = await deleteElement(element, graphqlClient, log);
503
- if (!success)
729
+ if (!success) {
730
+ persistProgress();
504
731
  return;
732
+ }
733
+ removeElementRecordById(great, element.id);
505
734
  log(chalk.green(`✓ Deleted ${element.id}`));
506
735
  deletedCount++;
507
736
  }
508
- 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);
509
746
  };
510
747
  export default (program, apiToken) => program
511
748
  .command('push')