@jungvonmatt/contentful-ssg 1.7.1 → 1.7.5-alpha.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/README.md CHANGED
@@ -47,7 +47,7 @@ npx cssg init --typescript
47
47
  | spaceId | `string` | `undefined` | Contentful Space id |
48
48
  | environmentId | `string` | `'master'` | Contentful Environment id |
49
49
  | format | `string`\|`function`\|`object` | `'yaml'` | File format ( `yaml`, `toml`, `md`, `json`) You can add a function returning the format or you can add a mapping object like `{yaml: [glob pattern]}` ([pattern](https://github.com/micromatch/micromatch) should match the directory) |
50
- | plugins | `[string]`\|`[[string, options]]`\|`[{resolve:'string', options:{}}]` | `[]` | Add plugins to contentful-ssg. See [Plugins](#plugins) |
50
+ | plugins | `[string]`\|`[[string, options]]`\|`[{resolve:'string', options:{}}]` | `[]` | Add plugins to contentful-ssg. See [Plugins](#plugins) |
51
51
  | directory | `string` | `'./content'` | Base directory for content files. |
52
52
  | validate | `function` | `undefined` | Pass `function(transformContext, runtimeContext){...}` to validate an entry. Return `false` to skip the entry completely. Without a validate function entries with a missing required field are skipped. |
53
53
  | transform | `function` | `undefined` | Pass `function(transformContext, runtimeContext){...}` to modify the stored object. Return `undefined` to skip the entry completely. (no file will be written) |
@@ -71,6 +71,7 @@ plugins: ['my-plugin-package', './plugins/my-local-plugin]
71
71
  All plugins can have options specified by wrapping the name and an options object in an array inside your config or by using a more verbose object notation.
72
72
 
73
73
  For specifying no options, these are all equivalent:
74
+
74
75
  ```js
75
76
  {
76
77
  "plugins": ["my-plugin", ["my-plugin"], ["my-plugin", {}], {resolve: "my-plugin", options: {}}]
@@ -78,6 +79,7 @@ For specifying no options, these are all equivalent:
78
79
  ```
79
80
 
80
81
  To specify an option, pass an object with the keys as the option names.
82
+
81
83
  ```js
82
84
  {
83
85
  "plugins": [
@@ -225,7 +227,6 @@ itself is waiting for the current entry to be transformed.
225
227
 
226
228
  // Do something usefull with the transformed data
227
229
  // which you can't do with context.entryMap.get('<contentful-id>')
228
-
229
230
  } catch (error) {
230
231
  // Entry isn't available, the transform method for the entry throws an error
231
232
  // or we encountered a cyclic dependency
@@ -245,10 +246,25 @@ npx cssg fetch
245
246
  ```
246
247
 
247
248
  To see all available command line options call
249
+
248
250
  ```bash
249
251
  npx cssg help fetch
250
252
  ```
251
253
 
254
+ ### watch
255
+
256
+ Same as `fetch` but also starts a webserver listening for changes and registers a contentful webhook
257
+
258
+ ```bash
259
+ npx cssg watch
260
+ ```
261
+
262
+ To see all available command line options call
263
+
264
+ ```bash
265
+ npx cssg help watch
266
+ ```
267
+
252
268
  ## Example configuration
253
269
 
254
270
  ### Grow
@@ -1,11 +1,11 @@
1
- import type { Config, RuntimeContext, TransformContext } from '../types.js';
1
+ import type { Entry, Config, RuntimeContext, TransformContext } from '../types.js';
2
2
  export declare const readFixture: (file: any) => Promise<any>;
3
3
  export declare const readFixtureSync: (file: any) => any;
4
4
  export declare const getContent: () => Promise<{
5
- entries: any;
6
- assets: any;
7
- contentTypes: any;
8
- locales: any;
5
+ entries: Entry[];
6
+ assets: import("contentful").Asset[];
7
+ contentTypes: import("contentful").ContentType[];
8
+ locales: import("contentful").Locale[];
9
9
  assetLink: {
10
10
  sys: {
11
11
  id: string;
@@ -20,8 +20,10 @@ export declare const getContent: () => Promise<{
20
20
  linkType: string;
21
21
  };
22
22
  };
23
- entry: any;
24
- asset: any;
23
+ entry: Entry;
24
+ asset: import("contentful").Asset;
25
+ assetMap: Map<string, import("contentful").Asset>;
26
+ entryMap: Map<string, Entry>;
25
27
  }>;
26
28
  export declare const getConfig: (fixture?: Partial<Config>) => Config;
27
29
  export declare const getRuntimeContext: (fixture?: Partial<RuntimeContext>) => RuntimeContext;
@@ -19,10 +19,10 @@ export const readFixtureSync = (file) => {
19
19
  return cache.get(file);
20
20
  };
21
21
  export const getContent = async () => {
22
- const assets = await readFixture('assets.json');
23
- const entries = await readFixture('entries.json');
24
- const locales = await readFixture('locales.json');
25
- const contentTypes = await readFixture('content_types.json');
22
+ const assets = (await readFixture('assets.json'));
23
+ const entries = (await readFixture('entries.json'));
24
+ const locales = (await readFixture('locales.json'));
25
+ const contentTypes = (await readFixture('content_types.json'));
26
26
  const [entry] = entries;
27
27
  const [asset] = assets;
28
28
  const assetLink = {
@@ -39,7 +39,20 @@ export const getContent = async () => {
39
39
  linkType: LINK_TYPE_ENTRY,
40
40
  },
41
41
  };
42
- return { entries, assets, contentTypes, locales, assetLink, entryLink, entry, asset };
42
+ const assetMap = new Map(assets.map((asset) => [asset.sys.id, asset]));
43
+ const entryMap = new Map(entries.map((entry) => [entry.sys.id, entry]));
44
+ return {
45
+ entries,
46
+ assets,
47
+ contentTypes,
48
+ locales,
49
+ assetLink,
50
+ entryLink,
51
+ entry,
52
+ asset,
53
+ assetMap,
54
+ entryMap,
55
+ };
43
56
  };
44
57
  export const getConfig = (fixture = {}) => ({
45
58
  directory: 'test',
package/dist/cli.js CHANGED
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import path from 'path';
3
3
  import chalk from 'chalk';
4
+ import ngrok from 'ngrok';
5
+ import getPort from 'get-port';
6
+ import exitHook from 'async-exit-hook';
4
7
  import { existsSync } from 'fs';
5
8
  import { readFile } from 'fs/promises';
6
9
  import { outputFile } from 'fs-extra';
@@ -10,8 +13,10 @@ import dotenv from 'dotenv';
10
13
  import dotenvExpand from 'dotenv-expand';
11
14
  import { logError, confirm, askAll, askMissing } from './lib/ui.js';
12
15
  import { omitKeys } from './lib/object.js';
16
+ import { getApp } from './server/index.js';
13
17
  import { getConfig, getEnvironmentConfig } from './lib/config.js';
14
18
  import { run } from './index.js';
19
+ import { addWatchWebhook, resetSync } from './lib/contentful.js';
15
20
  const env = dotenv.config();
16
21
  dotenvExpand(env);
17
22
  const parseFetchArgs = (cmd) => ({
@@ -90,8 +95,50 @@ program
90
95
  .option('-v, --verbose', 'Verbose output')
91
96
  .option('--ignore-errors', 'No error return code when transform has errors')
92
97
  .action(actionRunner(async (cmd) => {
98
+ await resetSync();
93
99
  const config = await getConfig(parseFetchArgs(cmd || {}));
94
100
  const verified = await askMissing(config);
95
101
  return run(verified);
96
102
  }));
103
+ program
104
+ .command('watch')
105
+ .description('Fetch content objects && watch for changes')
106
+ .option('-p, --preview', 'Fetch with preview mode')
107
+ .option('-v, --verbose', 'Verbose output')
108
+ .option('--url <url>', 'Url where the the server is reachable from the outside')
109
+ .option('--ignore-errors', 'No error return code when transform has errors')
110
+ .action(actionRunner(async (cmd) => {
111
+ await resetSync();
112
+ const config = await getConfig(parseFetchArgs(cmd || {}));
113
+ const verified = await askMissing(config);
114
+ let prev = await run({ ...verified, sync: true });
115
+ let port = await getPort({ port: 1314 });
116
+ if (cmd.url) {
117
+ const url = new URL(cmd.url);
118
+ port = url.port || url.protocol === 'https:' ? 443 : 80;
119
+ }
120
+ const app = getApp(async () => {
121
+ prev = await run({ ...verified, sync: true }, prev);
122
+ });
123
+ const server = app.listen(port);
124
+ const stopServer = async () => new Promise((resolve, reject) => {
125
+ server.close((err) => {
126
+ if (err) {
127
+ reject(err);
128
+ }
129
+ else {
130
+ resolve(true);
131
+ }
132
+ });
133
+ });
134
+ const url = cmd.url || (await ngrok.connect(port));
135
+ console.log(`\n Listening for hooks on ${chalk.cyan(url)}\n`);
136
+ const webhook = await addWatchWebhook(verified, url);
137
+ exitHook(async (cb) => {
138
+ await webhook.delete();
139
+ await stopServer();
140
+ await resetSync();
141
+ cb();
142
+ });
143
+ }));
97
144
  program.parse(process.argv);
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- import type { Config } from './types.js';
2
- export declare const run: (config: Config) => Promise<void>;
1
+ import type { Config, RunResult, RuntimeContext } from './types.js';
2
+ export declare const cleanupPrevData: (ctx: RuntimeContext, prev: RunResult) => void;
3
+ export declare const run: (config: Config, prev?: RunResult) => Promise<RunResult>;
package/dist/index.js CHANGED
@@ -1,14 +1,15 @@
1
- import Listr from 'listr';
2
- import { BehaviorSubject } from 'rxjs';
3
1
  import chalk from 'chalk';
4
- import { getContentTypeId, getContentId } from './lib/contentful.js';
5
- import { setup } from './tasks/setup.js';
2
+ import Listr from 'listr';
3
+ import { ReplaySubject } from 'rxjs';
4
+ import { getContentId, getContentTypeId, isSyncRequest } from './lib/contentful.js';
5
+ import { ValidationError } from './lib/error.js';
6
+ import { collectParentValues, collectValues, waitFor } from './lib/utils.js';
6
7
  import { fetch } from './tasks/fetch.js';
7
8
  import { localize } from './tasks/localize.js';
9
+ import { remove } from './tasks/remove.js';
10
+ import { setup } from './tasks/setup.js';
8
11
  import { transform } from './tasks/transform.js';
9
12
  import { write } from './tasks/write.js';
10
- import { collectParentValues, collectValues, waitFor } from './lib/utils.js';
11
- import { ValidationError } from './lib/error.js';
12
13
  class CustomListrRenderer {
13
14
  _tasks;
14
15
  constructor(tasks) {
@@ -38,7 +39,39 @@ class CustomListrRenderer {
38
39
  }
39
40
  }
40
41
  }
41
- export const run = async (config) => {
42
+ export const cleanupPrevData = (ctx, prev) => {
43
+ const entryMap = prev.localized?.[ctx.defaultLocale]?.entryMap ?? new Map();
44
+ ctx.data.deletedEntries =
45
+ ctx?.data?.deletedEntries?.map((entry) => {
46
+ if (entryMap.has(entry.sys.id)) {
47
+ const prevEntry = entryMap.get(entry.sys.id);
48
+ return { ...prevEntry, sys: { ...prevEntry.sys, ...entry.sys } };
49
+ }
50
+ return entry;
51
+ }) ?? [];
52
+ ctx.data.locales.forEach((locale) => {
53
+ if (ctx?.data?.deletedEntries?.length) {
54
+ ctx.data.deletedEntries.forEach((entry) => {
55
+ if (prev.localized?.[locale.code]?.entryMap.has(entry.sys.id)) {
56
+ prev.localized?.[locale.code]?.entryMap.delete(entry.sys.id);
57
+ prev.localized[locale.code].entries = Array.from(prev.localized?.[locale.code]?.entryMap.values());
58
+ }
59
+ });
60
+ }
61
+ if (ctx?.data?.deletedAssets?.length) {
62
+ ctx.data.deletedAssets.forEach((asset) => {
63
+ if (prev.localized?.[locale.code]?.assetMap.has(asset.sys.id)) {
64
+ prev.localized?.[locale.code]?.assetMap.delete(asset.sys.id);
65
+ prev.localized[locale.code].assets = Array.from(prev.localized?.[locale.code]?.assetMap.values());
66
+ }
67
+ });
68
+ }
69
+ });
70
+ };
71
+ export const run = async (config, prev = {
72
+ observables: {},
73
+ localized: {},
74
+ }) => {
42
75
  const tasks = new Listr([
43
76
  {
44
77
  title: 'Setup',
@@ -46,7 +79,10 @@ export const run = async (config) => {
46
79
  },
47
80
  {
48
81
  title: 'Pulling data from contentful',
49
- task: async (ctx) => fetch(ctx, config),
82
+ task: async (ctx) => {
83
+ await fetch(ctx, config);
84
+ cleanupPrevData(ctx, prev);
85
+ },
50
86
  },
51
87
  {
52
88
  title: 'Localize data',
@@ -57,6 +93,41 @@ export const run = async (config) => {
57
93
  skip: (ctx) => !ctx.hooks.has('before'),
58
94
  task: async (ctx) => ctx.hooks.before(),
59
95
  },
96
+ {
97
+ title: 'Remove deleted files',
98
+ skip: (ctx) => (ctx.data.deletedEntries ?? []).length === 0,
99
+ task: async (ctx) => {
100
+ const { locales = [], deletedEntries = [] } = ctx.data;
101
+ const tasks = locales.map((locale) => ({
102
+ title: `${locale.code}`,
103
+ task: async () => {
104
+ const data = ctx.localized.get(locale.code);
105
+ if (!prev?.observables?.[locale.code]) {
106
+ prev.observables[locale.code] = new ReplaySubject();
107
+ }
108
+ const subject = prev.observables[locale.code];
109
+ const observable = subject.asObservable();
110
+ const promises = deletedEntries.map(async (entry) => {
111
+ const id = getContentId(entry);
112
+ const contentTypeId = getContentTypeId(entry);
113
+ const utils = {};
114
+ const transformContext = {
115
+ ...data,
116
+ id,
117
+ contentTypeId,
118
+ entry,
119
+ locale,
120
+ observable,
121
+ utils,
122
+ };
123
+ return remove(transformContext, ctx, config);
124
+ });
125
+ return Promise.all(promises);
126
+ },
127
+ }));
128
+ return new Listr(tasks, { concurrent: true });
129
+ },
130
+ },
60
131
  {
61
132
  title: 'Writing files',
62
133
  task: async (ctx) => {
@@ -64,10 +135,24 @@ export const run = async (config) => {
64
135
  const tasks = locales.map((locale) => ({
65
136
  title: `${locale.code}`,
66
137
  task: async () => {
67
- const subject = new BehaviorSubject(null);
68
- const observable = subject.asObservable();
69
138
  const data = ctx.localized.get(locale.code);
70
139
  const { entries = [] } = data || {};
140
+ if (!prev?.observables?.[locale.code]) {
141
+ prev.observables[locale.code] = new ReplaySubject();
142
+ }
143
+ const subject = prev.observables[locale.code];
144
+ const observable = subject.asObservable();
145
+ data.assetMap = new Map([
146
+ ...(prev?.localized[locale.code]?.assetMap ?? new Map()),
147
+ ...data.assetMap,
148
+ ]);
149
+ data.entryMap = new Map([
150
+ ...(prev?.localized[locale.code]?.entryMap ?? new Map()),
151
+ ...data.entryMap,
152
+ ]);
153
+ data.entries = Array.from(data.entryMap.values());
154
+ data.assets = Array.from(data.assetMap.values());
155
+ prev.localized[locale.code] = data;
71
156
  const promises = entries.map(async (entry) => {
72
157
  const id = getContentId(entry);
73
158
  const contentTypeId = getContentTypeId(entry);
@@ -107,6 +192,7 @@ export const run = async (config) => {
107
192
  else {
108
193
  ctx.stats.addError(transformContext, error);
109
194
  }
195
+ await remove(transformContext, ctx, config);
110
196
  }
111
197
  });
112
198
  return Promise.all(promises);
@@ -122,13 +208,15 @@ export const run = async (config) => {
122
208
  },
123
209
  {
124
210
  title: 'Cleanup',
211
+ skip: () => isSyncRequest(),
125
212
  task: async (ctx) => ctx.fileManager.cleanup(),
126
213
  },
127
214
  ], { renderer: CustomListrRenderer });
128
215
  const ctx = await tasks.run();
129
216
  await ctx.stats.print();
130
- console.log('\n---------------------------------------------');
131
- if (ctx.stats.errors?.length && !config.ignoreErrors) {
217
+ console.log('\n -------------------------------------------');
218
+ if (!ctx.config.sync && ctx.stats.errors?.length && !config.ignoreErrors) {
132
219
  process.exit(1);
133
220
  }
221
+ return prev;
134
222
  };
@@ -1,4 +1,4 @@
1
- import type { Space } from 'contentful-management/types';
1
+ import type { Space, CreateWebhooksProps } from 'contentful-management/types';
2
2
  import type { ContentfulConfig, FieldSettings, Node, Entry, ContentType } from '../types.js';
3
3
  import contentful from 'contentful';
4
4
  export declare const FIELD_TYPE_SYMBOL = "Symbol";
@@ -15,6 +15,7 @@ export declare const FIELD_TYPE_OBJECT = "Object";
15
15
  export declare const LINK_TYPE_ASSET = "Asset";
16
16
  export declare const LINK_TYPE_ENTRY = "Entry";
17
17
  export declare const MAX_ALLOWED_LIMIT = 1000;
18
+ export declare const SYNC_TOKEN_FILENAME = ".contentful_sync.lock";
18
19
  export declare const getContentTypeId: <T extends contentful.Asset | Entry | contentful.Entry<unknown>>(node: T) => string;
19
20
  export declare const getEnvironmentId: <T extends Node>(node: T) => string;
20
21
  export declare const getContentId: <T extends contentful.ContentType | contentful.Asset | Entry | contentful.Entry<unknown>>(node: T) => string;
@@ -24,12 +25,29 @@ export declare const getEnvironments: (options: ContentfulConfig) => Promise<imp
24
25
  export declare const getEnvironment: (options: ContentfulConfig) => Promise<import("contentful-management/types").Environment>;
25
26
  export declare const getApiKey: (options: ContentfulConfig) => Promise<string>;
26
27
  export declare const getPreviewApiKey: (options: ContentfulConfig) => Promise<string>;
28
+ export declare const getWebhooks: (options: ContentfulConfig) => Promise<import("contentful-management/types").WebHooks[]>;
29
+ export declare const addWebhook: (options: ContentfulConfig, id: string, data: CreateWebhooksProps) => Promise<import("contentful-management/types").WebHooks>;
30
+ export declare const deleteWebhook: (options: ContentfulConfig, id: string) => Promise<void>;
31
+ export declare const addWatchWebhook: (options: ContentfulConfig, url: string) => Promise<import("contentful-management/types").WebHooks>;
32
+ export declare const isSyncRequest: () => boolean;
33
+ export declare const resetSync: () => Promise<true | void>;
27
34
  export declare const getContent: (options: ContentfulConfig) => Promise<{
35
+ entries: contentful.Entry<any>[];
36
+ assets: contentful.Asset[];
37
+ deletedEntries: contentful.Entry<any>[];
38
+ deletedAssets: contentful.Asset[];
39
+ contentTypes: contentful.ContentType[];
40
+ locales: contentful.Locale[];
41
+ } | {
28
42
  entries: Entry[];
29
43
  assets: contentful.Asset[];
30
44
  contentTypes: contentful.ContentType[];
31
45
  locales: contentful.Locale[];
46
+ deletedEntries?: undefined;
47
+ deletedAssets?: undefined;
32
48
  }>;
49
+ export declare const getEntriesLinkedToEntry: (options: ContentfulConfig, id: string) => Promise<Entry[]>;
50
+ export declare const getEntriesLinkedToAsset: (options: ContentfulConfig, id: string) => Promise<Entry[]>;
33
51
  export declare const isContentfulObject: (obj: any) => boolean;
34
52
  export declare const isLink: (obj: any) => boolean;
35
53
  export declare const isAssetLink: (obj: any) => boolean;
@@ -1,3 +1,7 @@
1
+ import { existsSync } from 'fs';
2
+ import { hostname } from 'os';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+ import { readFile, unlink, writeFile } from 'fs/promises';
1
5
  import contentful from 'contentful';
2
6
  import contentfulManagement from 'contentful-management';
3
7
  let client;
@@ -16,6 +20,7 @@ export const FIELD_TYPE_OBJECT = 'Object';
16
20
  export const LINK_TYPE_ASSET = 'Asset';
17
21
  export const LINK_TYPE_ENTRY = 'Entry';
18
22
  export const MAX_ALLOWED_LIMIT = 1000;
23
+ export const SYNC_TOKEN_FILENAME = '.contentful_sync.lock';
19
24
  export const getContentTypeId = (node) => node?.sys?.contentType?.sys?.id ?? 'unknown';
20
25
  export const getEnvironmentId = (node) => node?.sys?.environment?.sys?.id ?? 'unknown';
21
26
  export const getContentId = (node) => node?.sys?.id ?? 'unknown';
@@ -89,6 +94,68 @@ export const getPreviewApiKey = async (options) => {
89
94
  const { accessToken: previewAccessToken } = previewApiKey;
90
95
  return previewAccessToken;
91
96
  };
97
+ export const getWebhooks = async (options) => {
98
+ const space = await getSpace(options);
99
+ const { items: webhooks = [] } = await space.getWebhooks();
100
+ return webhooks;
101
+ };
102
+ export const addWebhook = async (options, id, data) => {
103
+ const space = await getSpace(options);
104
+ return space.createWebhookWithId(id, data);
105
+ };
106
+ export const deleteWebhook = async (options, id) => {
107
+ const space = await getSpace(options);
108
+ const webhook = await space.getWebhook(id);
109
+ return webhook.delete();
110
+ };
111
+ export const addWatchWebhook = async (options, url) => {
112
+ let topics = [
113
+ 'ContentType.publish',
114
+ 'ContentType.unpublish',
115
+ 'ContentType.delete',
116
+ 'Entry.archive',
117
+ 'Entry.unarchive',
118
+ 'Entry.publish',
119
+ 'Entry.unpublish',
120
+ 'Entry.delete',
121
+ 'Asset.archive',
122
+ 'Asset.unarchive',
123
+ 'Asset.publish',
124
+ 'Asset.unpublish',
125
+ 'Asset.delete',
126
+ ];
127
+ if (options.preview) {
128
+ topics = [
129
+ ...topics,
130
+ 'ContentType.save',
131
+ 'Entry.save',
132
+ 'Entry.auto_save',
133
+ 'Asset.save',
134
+ 'Asset.auto_save',
135
+ ];
136
+ }
137
+ const uuid = uuidv4();
138
+ return addWebhook(options, uuid, {
139
+ name: `contentful-ssg (${hostname()})`,
140
+ url,
141
+ httpBasicUsername: null,
142
+ topics,
143
+ filters: [
144
+ {
145
+ equals: [
146
+ {
147
+ doc: 'sys.environment.sys.id',
148
+ },
149
+ options.environmentId,
150
+ ],
151
+ },
152
+ ],
153
+ transformation: {
154
+ includeContentLength: true,
155
+ },
156
+ headers: [],
157
+ });
158
+ };
92
159
  const pagedGet = async (apiClient, { method, skip = 0, aggregatedResponse = null, query = null }) => {
93
160
  const fullQuery = {
94
161
  skip,
@@ -115,6 +182,25 @@ const pagedGet = async (apiClient, { method, skip = 0, aggregatedResponse = null
115
182
  }
116
183
  return aggregatedResponse;
117
184
  };
185
+ const sync = async (apiClient) => {
186
+ const options = { initial: true };
187
+ if (existsSync(SYNC_TOKEN_FILENAME)) {
188
+ options.nextSyncToken = await readFile(SYNC_TOKEN_FILENAME, 'utf8');
189
+ delete options.initial;
190
+ }
191
+ const response = (await apiClient.sync(options));
192
+ if (response.nextSyncToken) {
193
+ await writeFile(SYNC_TOKEN_FILENAME, response.nextSyncToken);
194
+ }
195
+ return response;
196
+ };
197
+ export const isSyncRequest = () => existsSync(SYNC_TOKEN_FILENAME);
198
+ export const resetSync = async () => {
199
+ if (isSyncRequest()) {
200
+ return unlink(SYNC_TOKEN_FILENAME);
201
+ }
202
+ return true;
203
+ };
118
204
  export const getContent = async (options) => {
119
205
  const apiClient = getClient(options);
120
206
  const { items: locales } = await pagedGet(apiClient, {
@@ -123,6 +209,10 @@ export const getContent = async (options) => {
123
209
  const { items: contentTypes } = await pagedGet(apiClient, {
124
210
  method: 'getContentTypes',
125
211
  });
212
+ if (options.sync) {
213
+ const { entries, assets, deletedEntries, deletedAssets } = await sync(apiClient);
214
+ return { entries, assets, deletedEntries, deletedAssets, contentTypes, locales };
215
+ }
126
216
  const { items: entries } = await pagedGet(apiClient, {
127
217
  method: 'getEntries',
128
218
  });
@@ -131,6 +221,22 @@ export const getContent = async (options) => {
131
221
  });
132
222
  return { entries, assets, contentTypes, locales };
133
223
  };
224
+ export const getEntriesLinkedToEntry = async (options, id) => {
225
+ const apiClient = getClient(options);
226
+ const { items: entries } = await pagedGet(apiClient, {
227
+ method: 'getEntries',
228
+ query: { links_to_entry: id },
229
+ });
230
+ return entries;
231
+ };
232
+ export const getEntriesLinkedToAsset = async (options, id) => {
233
+ const apiClient = getClient(options);
234
+ const { items: entries } = await pagedGet(apiClient, {
235
+ method: 'getEntries',
236
+ query: { links_to_asset: id },
237
+ });
238
+ return entries;
239
+ };
134
240
  export const isContentfulObject = (obj) => Object.prototype.toString.call(obj) === '[object Object]' && Object.keys(obj).includes('sys');
135
241
  export const isLink = (obj) => isContentfulObject(obj) && obj.sys.type === FIELD_TYPE_LINK;
136
242
  export const isAssetLink = (obj) => isLink(obj) && obj.sys.linkType === LINK_TYPE_ASSET;
@@ -0,0 +1,7 @@
1
+ declare module 'http' {
2
+ interface IncomingHttpHeaders {
3
+ 'x-contentful-topic': 'ContentManagement.ContentType.create' | 'ContentManagement.ContentType.save' | 'ContentManagement.ContentType.publish' | 'ContentManagement.ContentType.unpublish' | 'ContentManagement.ContentType.delete' | 'ContentManagement.Entry.create' | 'ContentManagement.Entry.save' | 'ContentManagement.Entry.auto_save' | 'ContentManagement.Entry.archive' | 'ContentManagement.Entry.unarchive' | 'ContentManagement.Entry.publish' | 'ContentManagement.Entry.unpublish' | 'ContentManagement.Entry.delete' | 'ContentManagement.Asset.create' | 'ContentManagement.Asset.save' | 'ContentManagement.Asset.auto_save' | 'ContentManagement.Asset.archive' | 'ContentManagement.Asset.unarchive' | 'ContentManagement.Asset.publish' | 'ContentManagement.Asset.unpublish' | 'ContentManagement.Asset.delete';
4
+ 'X-Contentful-Webhook-Name': string;
5
+ }
6
+ }
7
+ export declare const getApp: (callback: () => Promise<void>) => import("express-serve-static-core").Express;
@@ -0,0 +1,29 @@
1
+ import express from 'express';
2
+ const app = express();
3
+ app.disable('x-powered-by');
4
+ app.use(express.urlencoded({ extended: true }));
5
+ app.use(express.json({
6
+ type: [
7
+ 'application/vnd.contentful.management.v1+json',
8
+ 'application/vnd.contentful.management.v1+json; charset=utf-8',
9
+ 'application/json',
10
+ 'application/json; charset=utf-8',
11
+ 'application/x-www-form-urlencoded',
12
+ 'application/x-www-form-urlencoded; charset=utf-8',
13
+ ],
14
+ }));
15
+ export const getApp = (callback) => {
16
+ app.get('/status', (_req, res) => res.status(200).send('ok'));
17
+ app.get('/', async (_req, res) => {
18
+ await callback();
19
+ return res.status(200).send('ok');
20
+ });
21
+ app.post('/', async (req, res) => {
22
+ if (!req.body.sys) {
23
+ return res.status(401).send('error');
24
+ }
25
+ await callback();
26
+ return res.status(200).send('ok');
27
+ });
28
+ return app;
29
+ };
@@ -1,7 +1,13 @@
1
- import { getContent, getFieldSettings } from '../lib/contentful.js';
1
+ import { getContent, getFieldSettings, getEntriesLinkedToEntry, getEntriesLinkedToAsset, } from '../lib/contentful.js';
2
2
  export const fetch = async (context, config) => {
3
3
  const content = await getContent(config);
4
4
  const { locales, contentTypes } = content;
5
+ const additionalEntriesPromise = [
6
+ ...(content?.deletedEntries?.map(async (entry) => getEntriesLinkedToEntry(config, entry.sys.id)) ?? []),
7
+ ...(content?.deletedAssets?.map(async (asset) => getEntriesLinkedToAsset(config, asset.sys.id)) ?? []),
8
+ ];
9
+ const additionalEntries = (await Promise.all(additionalEntriesPromise)).flat();
10
+ content.entries = [...(content?.entries ?? []), ...additionalEntries];
5
11
  const fieldSettings = getFieldSettings(contentTypes);
6
12
  const { code: defaultLocale } = locales.find((locale) => locale.default);
7
13
  context.defaultLocale = defaultLocale;
@@ -0,0 +1,2 @@
1
+ import { TransformContext, RuntimeContext, Config } from '../types';
2
+ export declare const remove: (transformContext: TransformContext, runtimeContext: RuntimeContext, config: Config) => Promise<void>;
@@ -0,0 +1,5 @@
1
+ import { getFilepath } from './write.js';
2
+ export const remove = async (transformContext, runtimeContext, config) => {
3
+ const filepath = await getFilepath(transformContext, runtimeContext, config);
4
+ await runtimeContext.fileManager.deleteFile(filepath);
5
+ };
@@ -1,2 +1,4 @@
1
1
  import type { TransformContext, RuntimeContext, Config } from '../types.js';
2
+ export declare const getFormat: (transformContext: TransformContext, runtimeContext: RuntimeContext, config: Config) => Promise<string>;
3
+ export declare const getFilepath: (transformContext: TransformContext, runtimeContext: RuntimeContext, config: Config) => Promise<string>;
2
4
  export declare const write: (transformContext: TransformContext, runtimeContext: RuntimeContext, config: Config) => Promise<void>;