@jungvonmatt/contentful-fakes 1.4.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/.env ADDED
@@ -0,0 +1,3 @@
1
+ CONTENTFUL_SPACE_ID=gpdredy5px7h
2
+ CONTENTFUL_DELIVERY_TOKEN=e80ac10a81330387c6676abf6388fdee8814ccb9480e057c587b855dfd7b6beb
3
+ CONTENTFUL_PREVIEW_TOKEN=a3651d3c90791271f75cbb5387df8d23648d7905a29160bc0e7626c8ed0b6e8e
package/.eslintrc.cjs ADDED
@@ -0,0 +1,16 @@
1
+ module.exports = {
2
+ root: true,
3
+ plugins: ['prettier'],
4
+ extends: ['xo', 'xo-typescript/space', 'plugin:prettier/recommended'],
5
+ parser: '@typescript-eslint/parser',
6
+ parserOptions: {
7
+ project: ['./tsconfig.json'],
8
+ tsconfigRootDir: __dirname,
9
+ extraFileExtensions: ['.cjs'],
10
+ },
11
+ ignorePatterns: ['**/*.cjs', 'src/**/*.test.ts', 'src/__test__/*'],
12
+ env: {
13
+ node: true,
14
+ jest: true,
15
+ }
16
+ };
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
+
6
+ # [1.4.0](https://github.com/jungvonmatt/contentful-ssg/compare/v1.3.2...v1.4.0) (2022-01-19)
7
+
8
+
9
+ ### Features
10
+
11
+ * 🎸 Adds new contentful-fakes package ([bb67694](https://github.com/jungvonmatt/contentful-ssg/commit/bb676946532281864c6355f294e984f792f9cfa3))
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Jung von Matt TECH (https://www.jvm.com/)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ [![NPM version][npm-image]][npm-url]
2
+
3
+ # JvM Contentful fake data generator
4
+
5
+ Generate fake data files based on your Contentful content models
6
+
7
+ ## Getting started
8
+
9
+ ### Install
10
+
11
+ ```bash
12
+ npm install --save-dev @jungvonmatt/contentful-fakes
13
+ ```
14
+
15
+ ## Commands
16
+
17
+ ### help
18
+
19
+ ```bash
20
+ npx contentful-fakes help [command]
21
+ ```
22
+
23
+ ### create
24
+
25
+ Fetch all content entries and store them as yaml in the configured directory
26
+
27
+ ```bash
28
+ npx contentful-fakes create [options]
29
+ ```
30
+
31
+ #### options
32
+
33
+ ```
34
+ -c, --content-type` `<content-type...> Specify content-types
35
+ -e, --extension <extension> Specify output format (default: "yaml")
36
+ -o, --output-directory <directory> Specify output directory (default: "data")
37
+ ```
38
+
39
+ [npm-url]: https://www.npmjs.com/package/@jungvonmatt/contentful-fakes
40
+ [npm-image]: https://img.shields.io/npm/v/@jungvonmatt/contentful-fakes.svg
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ import path from 'path';
3
+ import { existsSync } from 'fs';
4
+ import { outputFile } from 'fs-extra';
5
+ import { Command } from 'commander';
6
+ import dotenv from 'dotenv';
7
+ import dotenvExpand from 'dotenv-expand';
8
+ import { getConfig } from '@jungvonmatt/contentful-ssg/lib/config';
9
+ import { forEachAsync } from '@jungvonmatt/contentful-ssg/lib/array';
10
+ import { stringify } from '@jungvonmatt/contentful-ssg/converter';
11
+ import { logError, askMissing, confirm } from '@jungvonmatt/contentful-ssg/lib/ui';
12
+ import { createFakes } from './index.js';
13
+ const env = dotenv.config();
14
+ dotenvExpand(env);
15
+ const parseArgs = (cmd) => ({
16
+ environment: cmd.env,
17
+ verbose: Boolean(cmd.verbose),
18
+ contentTypes: cmd.contentType,
19
+ });
20
+ const errorHandler = (error, silence) => {
21
+ if (!silence) {
22
+ const { errors } = error;
23
+ logError(error);
24
+ (errors || []).forEach((error) => {
25
+ logError(error);
26
+ });
27
+ }
28
+ process.exit(1);
29
+ };
30
+ const actionRunner = (fn, log = true) => (...args) => fn(...args).catch((error) => errorHandler(error, !log));
31
+ const program = new Command();
32
+ program
33
+ .command('create')
34
+ .description('Create fake objects')
35
+ .option('-c, --content-type <content-type...>', 'Specify content-types')
36
+ .option('-e, --extension <extension>', 'Specify output format', 'yaml')
37
+ .option('-o, --output-directory <directory>', 'Specify output directory', 'data')
38
+ .option('-f, --force', 'Overwrite')
39
+ .action(actionRunner(async (cmd) => {
40
+ const contentTypes = cmd?.contentType ?? [];
41
+ const force = cmd?.force ?? false;
42
+ const format = cmd?.extension ?? '';
43
+ const outputDirectory = cmd?.outputDirectory ?? '';
44
+ const config = await getConfig(parseArgs(cmd));
45
+ const verified = await askMissing(config);
46
+ const fakes = await createFakes(verified, contentTypes);
47
+ await forEachAsync(Object.entries(fakes), async ([contentTypeId, fakeData]) => {
48
+ const [defaultData, minData] = fakeData;
49
+ const dir = path.join(outputDirectory, contentTypeId);
50
+ const filenameDefault = path.join(dir, `default.${format}`);
51
+ const filenameMin = path.join(dir, `min.${format}`);
52
+ if (!existsSync(filenameDefault) ||
53
+ force ||
54
+ (await confirm(`Overwrite file: ${filenameDefault}`))) {
55
+ await outputFile(filenameDefault, stringify(defaultData, format));
56
+ }
57
+ if (!existsSync(filenameMin) ||
58
+ force ||
59
+ (await confirm(`Overwrite file: ${filenameMin}`))) {
60
+ await outputFile(filenameMin, stringify(minData, format));
61
+ }
62
+ });
63
+ }));
64
+ program.parse(process.argv);
@@ -0,0 +1,2 @@
1
+ import { ContentfulConfig, KeyValueMap } from '@jungvonmatt/contentful-ssg';
2
+ export declare function createFakes(config: ContentfulConfig, contentTypeIds: string[]): Promise<Record<string, KeyValueMap[]>>;
package/dist/index.js ADDED
@@ -0,0 +1,27 @@
1
+ import { getEnvironment } from '@jungvonmatt/contentful-ssg/lib/contentful';
2
+ import { getMockData } from './lib/faker.js';
3
+ export async function createFakes(config, contentTypeIds) {
4
+ const environment = await getEnvironment(config);
5
+ const { items: contentTypes } = await environment.getContentTypes();
6
+ const { items } = await environment.getEditorInterfaces();
7
+ const interfaces = Object.fromEntries(items
8
+ .filter((item) => !contentTypeIds.length || contentTypeIds.includes(item.sys.contentType.sys.id))
9
+ .map((item) => {
10
+ const { id } = item.sys.contentType.sys;
11
+ const contentType = contentTypes.find((ct) => ct.sys.id === id);
12
+ const { controls } = item;
13
+ const fields = controls.map((control) => {
14
+ const { fieldId } = control;
15
+ const fieldSettings = contentType.fields.find((field) => field.id === fieldId);
16
+ return { interface: control, settings: fieldSettings };
17
+ });
18
+ return [id, fields];
19
+ }));
20
+ const data = await getMockData(interfaces);
21
+ return Object.fromEntries(Object.entries(interfaces).map(([contentTypeId, fields]) => {
22
+ const { [contentTypeId]: fakeData = {} } = data;
23
+ const required = Object.fromEntries(fields.map((field) => [field.settings.id, field.settings.required]));
24
+ const minData = Object.fromEntries(Object.entries(fakeData).filter(([fieldId]) => required?.[fieldId]));
25
+ return [contentTypeId, [fakeData, minData]];
26
+ }));
27
+ }
@@ -0,0 +1,8 @@
1
+ import { ContentFields, KeyValueMap, Control } from 'contentful-management/types';
2
+ export declare type FieldInfo = {
3
+ interface: Control;
4
+ settings: Partial<ContentFields>;
5
+ };
6
+ export declare type ContentTypes = Record<string, FieldInfo[]>;
7
+ export declare const getMockData: (contentTypes: ContentTypes) => Promise<KeyValueMap>;
8
+ export declare const createData: (fields: FieldInfo[]) => Promise<KeyValueMap>;
@@ -0,0 +1,318 @@
1
+ import { FIELD_TYPE_SYMBOL, FIELD_TYPE_TEXT, FIELD_TYPE_RICHTEXT, FIELD_TYPE_NUMBER, FIELD_TYPE_INTEGER, FIELD_TYPE_DATE, FIELD_TYPE_LOCATION, FIELD_TYPE_ARRAY, FIELD_TYPE_BOOLEAN, FIELD_TYPE_LINK, FIELD_TYPE_OBJECT, LINK_TYPE_ASSET, LINK_TYPE_ENTRY, } from '@jungvonmatt/contentful-ssg/lib/contentful';
2
+ import faker from 'faker';
3
+ import casual from 'casual';
4
+ import RandExp from 'randexp';
5
+ import { oneOf, randomInt } from './helper.js';
6
+ export const getMockData = async (contentTypes) => {
7
+ const dataEntries = await Promise.all(Object.entries(contentTypes).map(async ([name, fields]) => [name, await createData(fields)]));
8
+ return Object.fromEntries(dataEntries);
9
+ };
10
+ export const createData = async (fields) => {
11
+ const entries = await Promise.all(fields.map(async (field) => {
12
+ const id = field?.settings?.id || 'undefined';
13
+ switch (field.settings.type) {
14
+ case FIELD_TYPE_SYMBOL:
15
+ return [id, await getSymbolFake(field)];
16
+ case FIELD_TYPE_INTEGER:
17
+ return [id, await getIntegerFake(field)];
18
+ case FIELD_TYPE_NUMBER:
19
+ return [id, await getNumberFake(field)];
20
+ case FIELD_TYPE_TEXT:
21
+ return [id, await getTextFake(field)];
22
+ case FIELD_TYPE_LINK:
23
+ return [id, await getLinkFake(field)];
24
+ case FIELD_TYPE_ARRAY:
25
+ return [id, await getArrayFake(field)];
26
+ case FIELD_TYPE_BOOLEAN:
27
+ return [id, faker.datatype.boolean()];
28
+ case FIELD_TYPE_DATE:
29
+ return [id, faker.date.future()];
30
+ case FIELD_TYPE_RICHTEXT:
31
+ return [id, await getRichtextFake(field)];
32
+ case FIELD_TYPE_OBJECT:
33
+ return [id, {}];
34
+ case FIELD_TYPE_LOCATION:
35
+ return [
36
+ id,
37
+ {
38
+ lat: faker.address.latitude(),
39
+ lng: faker.address.longitude(),
40
+ },
41
+ ];
42
+ default:
43
+ return [id, undefined];
44
+ }
45
+ }));
46
+ return Object.fromEntries(entries);
47
+ };
48
+ const getRegexValue = (regex) => {
49
+ const { pattern, flags } = regex;
50
+ if (pattern === '^\\w[\\w.-]*@([\\w-]+\\.)+[\\w-]+$') {
51
+ return faker.internet.exampleEmail();
52
+ }
53
+ if (pattern.includes('\\/\\/(\\w+:{0,1}\\w*@)?(\\S+)(:[0-9]+)?(\\/|\\/([\\w#!:.?+=&%@!\\-/]))?$')) {
54
+ return faker.internet.url();
55
+ }
56
+ if (pattern === '^(0?[1-9]|1[012])[- /.](0?[1-9]|[12][0-9]|3[01])[- /.](19|20)?\\d\\d$') {
57
+ return casual.time('MM-DD-YYYY');
58
+ }
59
+ if (pattern === '^(0?[1-9]|[12][0-9]|3[01])[- /.](0?[1-9]|1[012])[- /.](19|20)?\\d\\d$') {
60
+ return casual.time('DD-MM-YYYY');
61
+ }
62
+ if (pattern === '^(0?[1-9]|1[012]):[0-5][0-9](:[0-5][0-9])?\\s*[aApP][mM]$') {
63
+ return casual.time('hh:mm a');
64
+ }
65
+ if (pattern === '^(0?[0-9]|1[0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?$') {
66
+ return casual.time('HH:mm');
67
+ }
68
+ if (pattern === '^\\d[ -.]?\\(?\\d\\d\\d\\)?[ -.]?\\d\\d\\d[ -.]?\\d\\d\\d\\d$') {
69
+ return faker.phone.phoneNumber();
70
+ }
71
+ if (pattern === '^\\d{5}$|^\\d{5}-\\d{4}$}') {
72
+ return faker.address.zipCode();
73
+ }
74
+ return new RandExp(pattern, flags);
75
+ };
76
+ const getTextForId = (id) => {
77
+ if ([
78
+ 'username',
79
+ 'first_name',
80
+ 'last_name',
81
+ 'full_name',
82
+ 'password',
83
+ 'name_prefix',
84
+ 'name_suffix',
85
+ 'company_name',
86
+ 'company_suffix',
87
+ 'catch_phrase',
88
+ 'phone',
89
+ 'ip',
90
+ 'domain',
91
+ 'url',
92
+ 'email',
93
+ 'user_agent',
94
+ 'country',
95
+ 'city',
96
+ 'zip',
97
+ 'street',
98
+ 'address',
99
+ 'address1',
100
+ 'address2',
101
+ 'state',
102
+ 'state_abbr',
103
+ 'latitude',
104
+ 'longitude',
105
+ 'building_number',
106
+ ].includes(id)) {
107
+ return casual?.[id]() ?? faker.lorem.word();
108
+ }
109
+ if (['uuid', 'uid', 'id'].includes(id)) {
110
+ return casual.uuid;
111
+ }
112
+ return undefined;
113
+ };
114
+ const getIntegerFake = async (field) => {
115
+ return getNumberFake(field, 1);
116
+ };
117
+ const getNumberFake = async (field, precision = 0.005) => {
118
+ const { validations = [] } = field.settings;
119
+ const { in: values } = validations.find((validation) => Boolean(validation?.in)) || {};
120
+ if (values) {
121
+ return oneOf(values);
122
+ }
123
+ const { range } = validations.find((validation) => Boolean(validation?.range)) || {};
124
+ if (range) {
125
+ const { min, max } = range;
126
+ if (typeof min !== 'undefined' && typeof max !== 'undefined') {
127
+ return faker.datatype.number({ min, max, precision });
128
+ }
129
+ if (typeof min !== 'undefined') {
130
+ return faker.datatype.number({ min, precision });
131
+ }
132
+ if (typeof max !== 'undefined') {
133
+ return faker.datatype.number({ max, precision });
134
+ }
135
+ }
136
+ return faker.datatype.number({ precision });
137
+ };
138
+ const getSymbolFake = async (field) => {
139
+ const { widgetId } = field.interface;
140
+ const { validations = [], id } = field.settings;
141
+ const { in: values } = validations.find((validation) => Boolean(validation?.in)) || {};
142
+ if (values) {
143
+ return oneOf(values);
144
+ }
145
+ let result = '';
146
+ if (widgetId === 'urlEditor') {
147
+ result = faker.internet.url();
148
+ }
149
+ if (widgetId === 'slugEditor') {
150
+ result = faker.lorem.slug();
151
+ }
152
+ const byId = getTextForId(id);
153
+ if (!result && byId) {
154
+ return byId;
155
+ }
156
+ if (!result && ['dropdown', 'radio'].includes(widgetId)) {
157
+ result = faker.lorem.word();
158
+ }
159
+ const { regexp } = validations.find((validation) => Boolean(validation?.regexp)) || {};
160
+ if (!result && regexp) {
161
+ result = getRegexValue(regexp);
162
+ }
163
+ if (!result) {
164
+ result = faker.lorem.sentence();
165
+ }
166
+ const { size } = validations.find((validation) => Boolean(validation?.size)) || {};
167
+ if (size) {
168
+ const { min = 0, max = 255 } = size;
169
+ const count = randomInt(min, max);
170
+ result = result.slice(0, count);
171
+ }
172
+ return result;
173
+ };
174
+ const getTextFake = async (field) => {
175
+ const { validations = [], id } = field.settings;
176
+ const { in: values } = validations.find((validation) => Boolean(validation?.in)) || {};
177
+ let result = '';
178
+ if (values) {
179
+ result = oneOf(values);
180
+ }
181
+ const byId = getTextForId(id);
182
+ if (!result && byId) {
183
+ result = byId;
184
+ }
185
+ const { regexp } = validations.find((validation) => Boolean(validation?.regexp)) || {};
186
+ if (!result && regexp) {
187
+ result = getRegexValue(regexp);
188
+ }
189
+ if (!result) {
190
+ result = faker.lorem.lines();
191
+ }
192
+ const { size } = validations.find((validation) => Boolean(validation?.size)) || {};
193
+ if (size) {
194
+ const { min = 0, max = 50000 } = size;
195
+ const count = randomInt(min, max);
196
+ result = result.slice(0, count);
197
+ }
198
+ return result;
199
+ };
200
+ const getAssetFake = async () => {
201
+ const sizes = [640, 480, 600, 800, 1024];
202
+ const width = oneOf(sizes);
203
+ const height = oneOf(sizes);
204
+ return {
205
+ mime_type: 'image/jpeg',
206
+ url: faker.image.image(width, height),
207
+ title: faker.lorem.words(),
208
+ description: faker.lorem.lines(),
209
+ width,
210
+ height,
211
+ file_size: faker.datatype.number({ min: 1000, max: 200000 }),
212
+ };
213
+ };
214
+ const getEntryScheme = async (field) => {
215
+ const { validations } = field.settings;
216
+ const { linkContentType = [] } = validations.find((validation) => Boolean(validation?.linkContentType)) || {};
217
+ if (linkContentType.length > 0) {
218
+ return {
219
+ id: 'default',
220
+ content_type: oneOf(linkContentType),
221
+ };
222
+ }
223
+ return {};
224
+ };
225
+ const getLinkFake = async (field) => {
226
+ if (field?.settings?.linkType === LINK_TYPE_ASSET) {
227
+ return getAssetFake();
228
+ }
229
+ return getEntryScheme(field);
230
+ };
231
+ const getRichtextFake = async (field) => {
232
+ const { validations } = field.settings;
233
+ const { enabledMarks: marks = [] } = validations.find((validation) => Boolean(validation?.enabledMarks)) || {};
234
+ const { enabledNodeTypes: nodeTypes = [] } = validations.find((validation) => Boolean(validation?.enabledNodeTypes)) || {};
235
+ return {
236
+ node_type: 'document',
237
+ data: {},
238
+ content: [
239
+ ...nodeTypes
240
+ .filter((nodeType) => nodeType.startsWith('heading'))
241
+ .map((nodeType) => ({
242
+ node_type: nodeType,
243
+ content: [
244
+ {
245
+ node_type: 'text',
246
+ value: nodeType,
247
+ marks: [],
248
+ data: {},
249
+ },
250
+ ],
251
+ data: {},
252
+ })),
253
+ ...marks.map((mark) => ({
254
+ node_type: 'paragraph',
255
+ content: [
256
+ {
257
+ node_type: 'text',
258
+ value: mark,
259
+ marks: [mark],
260
+ data: {},
261
+ },
262
+ ],
263
+ data: {},
264
+ })),
265
+ {
266
+ node_type: 'paragraph',
267
+ content: [
268
+ {
269
+ node_type: 'text',
270
+ value: faker.lorem.sentence(),
271
+ marks: [],
272
+ data: {},
273
+ },
274
+ ],
275
+ data: {},
276
+ },
277
+ ],
278
+ };
279
+ };
280
+ const getArrayFake = async (field) => {
281
+ const { validations } = field.settings;
282
+ const { size } = validations.find((validation) => Boolean(validation?.size)) || {};
283
+ let count = randomInt(0, 10);
284
+ if (size) {
285
+ count = randomInt(size?.min ?? 0, size?.max ?? (size?.min ?? 0) + 10);
286
+ }
287
+ if (field.settings.items.type === FIELD_TYPE_SYMBOL) {
288
+ return Promise.all(Array(count)
289
+ .fill('')
290
+ .map(async () => getSymbolFake({
291
+ ...field,
292
+ settings: { ...field.settings.items, id: field.settings.id },
293
+ })));
294
+ }
295
+ if (field.settings.items.type === FIELD_TYPE_TEXT) {
296
+ return Promise.all(Array(count)
297
+ .fill('')
298
+ .map(async () => getTextFake({
299
+ ...field,
300
+ settings: { ...field.settings.items, id: field.settings.id },
301
+ })));
302
+ }
303
+ if (field.settings.items.type === FIELD_TYPE_LINK &&
304
+ field.settings.items.linkType === LINK_TYPE_ASSET) {
305
+ return Promise.all(Array(count)
306
+ .fill('')
307
+ .map(async () => getAssetFake()));
308
+ }
309
+ if (field.settings.items.type === FIELD_TYPE_LINK &&
310
+ field.settings.items.linkType === LINK_TYPE_ENTRY) {
311
+ return Promise.all(Array(count)
312
+ .fill('')
313
+ .map(async () => getEntryScheme({
314
+ ...field,
315
+ settings: { ...field.settings.items, id: field.settings.id },
316
+ })));
317
+ }
318
+ };
@@ -0,0 +1,2 @@
1
+ export declare const oneOf: <T = string>(array?: T[]) => T;
2
+ export declare const randomInt: (min?: number, max?: number) => number;
@@ -0,0 +1,9 @@
1
+ export const oneOf = (array = []) => {
2
+ const index = randomInt(0, array.length);
3
+ return array[index];
4
+ };
5
+ export const randomInt = (min = 0, max = 1000000) => {
6
+ min = Math.ceil(min || 0);
7
+ max = Math.floor(max || 1000000);
8
+ return Math.floor(Math.random() * (max - min)) + min;
9
+ };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@jungvonmatt/contentful-fakes",
3
+ "version": "1.4.0",
4
+ "description": "Create fake data based on contentful data models",
5
+ "main": "./dist/index.js",
6
+ "type": "module",
7
+ "typings": "./dist/types.d.ts",
8
+ "bin": {
9
+ "contentful-fakes": "./dist/cli.js"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "scripts": {
15
+ "clean": "rimraf ./dist",
16
+ "lint": "eslint --color src --fix --ext .ts",
17
+ "precompile": "npm run clean",
18
+ "compile": "tsc --build",
19
+ "postcompile": "chmod +x ./dist/cli.js",
20
+ "watch": "tsc --build --watch"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/jungvonmatt/contentful-ssg.git"
25
+ },
26
+ "keywords": [
27
+ "fake",
28
+ "data",
29
+ "contentful"
30
+ ],
31
+ "author": "Ben Zörb",
32
+ "license": "MIT",
33
+ "bugs": {
34
+ "url": "https://github.com/jungvonmatt/contentful-ssg/issues"
35
+ },
36
+ "homepage": "https://github.com/jungvonmatt/contentful-ssg#readme",
37
+ "dependencies": {
38
+ "@jungvonmatt/contentful-ssg": "^1.4.0",
39
+ "casual": "^1.6.2",
40
+ "faker": "^5.5.3",
41
+ "fs-extra": "^10.0.0",
42
+ "randexp": "^0.5.3"
43
+ },
44
+ "devDependencies": {
45
+ "@types/chance": "^1.1.3",
46
+ "@types/faker": "^6.6.9"
47
+ },
48
+ "gitHead": "0d323f9884ae8fa48049e25bcd127954ee7988b8"
49
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+
3
+ /* eslint-env node */
4
+ import path from 'path';
5
+ import { existsSync } from 'fs';
6
+ import { outputFile } from 'fs-extra';
7
+ import { Command } from 'commander';
8
+ import dotenv from 'dotenv';
9
+ import dotenvExpand from 'dotenv-expand';
10
+ import { ContentfulConfig } from '@jungvonmatt/contentful-ssg';
11
+ import { getConfig } from '@jungvonmatt/contentful-ssg/lib/config';
12
+ import { forEachAsync } from '@jungvonmatt/contentful-ssg/lib/array';
13
+ import { stringify } from '@jungvonmatt/contentful-ssg/converter';
14
+ import { logError, askMissing, confirm } from '@jungvonmatt/contentful-ssg/lib/ui';
15
+ import { createFakes } from './index.js';
16
+
17
+ const env = dotenv.config();
18
+ dotenvExpand(env);
19
+
20
+ type CommandArgs = {
21
+ outputDirectory: string;
22
+ extension: string;
23
+ contentType: string[];
24
+ env: string;
25
+ verbose: boolean;
26
+ force: boolean;
27
+ };
28
+
29
+ const parseArgs = (cmd: CommandArgs) => ({
30
+ environment: cmd.env,
31
+ verbose: Boolean(cmd.verbose),
32
+ contentTypes: cmd.contentType,
33
+ });
34
+
35
+ type CommandError = Error & {
36
+ errors?: Error[];
37
+ };
38
+ const errorHandler = (error: CommandError, silence: boolean) => {
39
+ if (!silence) {
40
+ const { errors } = error;
41
+ logError(error);
42
+ (errors || []).forEach((error) => {
43
+ logError(error);
44
+ });
45
+ }
46
+
47
+ process.exit(1);
48
+ };
49
+
50
+ const actionRunner =
51
+ (fn, log = true) =>
52
+ (...args) =>
53
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
54
+ fn(...args).catch((error) => errorHandler(error, !log));
55
+ const program = new Command();
56
+
57
+ program
58
+ .command('create')
59
+ .description('Create fake objects')
60
+ .option('-c, --content-type <content-type...>', 'Specify content-types')
61
+ .option('-e, --extension <extension>', 'Specify output format', 'yaml')
62
+ .option('-o, --output-directory <directory>', 'Specify output directory', 'data')
63
+ .option('-f, --force', 'Overwrite')
64
+ .action(
65
+ actionRunner(async (cmd: CommandArgs) => {
66
+ const contentTypes: string[] = cmd?.contentType ?? [];
67
+ const force: boolean = cmd?.force ?? false;
68
+ const format: string = cmd?.extension ?? '';
69
+ const outputDirectory: string = cmd?.outputDirectory ?? '';
70
+ const config = await getConfig(parseArgs(cmd));
71
+ const verified = await askMissing(config);
72
+ const fakes = await createFakes(verified as ContentfulConfig, contentTypes);
73
+ await forEachAsync(Object.entries(fakes), async ([contentTypeId, fakeData]) => {
74
+ const [defaultData, minData] = fakeData;
75
+ const dir = path.join(outputDirectory, contentTypeId);
76
+ const filenameDefault = path.join(dir, `default.${format}`);
77
+ const filenameMin = path.join(dir, `min.${format}`);
78
+ if (
79
+ !existsSync(filenameDefault) ||
80
+ force ||
81
+ (await confirm(`Overwrite file: ${filenameDefault}`))
82
+ ) {
83
+ await outputFile(filenameDefault, stringify(defaultData, format));
84
+ }
85
+
86
+ if (
87
+ !existsSync(filenameMin) ||
88
+ force ||
89
+ (await confirm(`Overwrite file: ${filenameMin}`))
90
+ ) {
91
+ await outputFile(filenameMin, stringify(minData, format));
92
+ }
93
+ });
94
+ })
95
+ );
96
+
97
+ program.parse(process.argv);
package/src/index.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { ContentfulConfig, KeyValueMap } from '@jungvonmatt/contentful-ssg';
2
+ import { getEnvironment } from '@jungvonmatt/contentful-ssg/lib/contentful';
3
+ import { getMockData, ContentTypes } from './lib/faker.js';
4
+
5
+ export async function createFakes(
6
+ config: ContentfulConfig,
7
+ contentTypeIds: string[]
8
+ ): Promise<Record<string, KeyValueMap[]>> {
9
+ const environment = await getEnvironment(config);
10
+ const { items: contentTypes } = await environment.getContentTypes();
11
+ const { items } = await environment.getEditorInterfaces();
12
+
13
+ const interfaces: ContentTypes = Object.fromEntries(
14
+ items
15
+ .filter(
16
+ (item) => !contentTypeIds.length || contentTypeIds.includes(item.sys.contentType.sys.id)
17
+ )
18
+ .map((item) => {
19
+ const { id } = item.sys.contentType.sys;
20
+ const contentType = contentTypes.find((ct) => ct.sys.id === id);
21
+ const { controls } = item;
22
+
23
+ const fields = controls.map((control) => {
24
+ const { fieldId } = control;
25
+ const fieldSettings = contentType.fields.find((field) => field.id === fieldId);
26
+ return { interface: control, settings: fieldSettings };
27
+ });
28
+ return [id, fields];
29
+ })
30
+ );
31
+
32
+ const data = await getMockData(interfaces);
33
+
34
+ return Object.fromEntries(
35
+ Object.entries(interfaces).map(([contentTypeId, fields]) => {
36
+ const { [contentTypeId]: fakeData = {} } = data;
37
+ const required = Object.fromEntries(
38
+ fields.map((field) => [field.settings.id, field.settings.required])
39
+ );
40
+
41
+ const minData = Object.fromEntries(
42
+ Object.entries(fakeData).filter(([fieldId]) => required?.[fieldId])
43
+ );
44
+ return [contentTypeId, [fakeData, minData]];
45
+ })
46
+ );
47
+ }
@@ -0,0 +1,451 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
3
+ /* eslint-disable @typescript-eslint/no-unsafe-return */
4
+ /* eslint-disable @typescript-eslint/naming-convention */
5
+ // See validations
6
+ // https://www.contentful.com/developers/docs/references/content-management-api/#/reference/content-types/content-type
7
+
8
+ import {
9
+ FIELD_TYPE_SYMBOL,
10
+ FIELD_TYPE_TEXT,
11
+ FIELD_TYPE_RICHTEXT,
12
+ FIELD_TYPE_NUMBER,
13
+ FIELD_TYPE_INTEGER,
14
+ FIELD_TYPE_DATE,
15
+ FIELD_TYPE_LOCATION,
16
+ FIELD_TYPE_ARRAY,
17
+ FIELD_TYPE_BOOLEAN,
18
+ FIELD_TYPE_LINK,
19
+ FIELD_TYPE_OBJECT,
20
+ LINK_TYPE_ASSET,
21
+ LINK_TYPE_ENTRY,
22
+ } from '@jungvonmatt/contentful-ssg/lib/contentful';
23
+
24
+ import { ContentFields, KeyValueMap, Control } from 'contentful-management/types';
25
+
26
+ import faker from 'faker';
27
+ import casual from 'casual';
28
+ import RandExp from 'randexp';
29
+ import { oneOf, randomInt } from './helper.js';
30
+
31
+ export type FieldInfo = {
32
+ interface: Control;
33
+ settings: Partial<ContentFields>;
34
+ };
35
+
36
+ export type ContentTypes = Record<string, FieldInfo[]>;
37
+
38
+ export const getMockData = async (contentTypes: ContentTypes): Promise<KeyValueMap> => {
39
+ const dataEntries = await Promise.all(
40
+ Object.entries(contentTypes).map(async ([name, fields]) => [name, await createData(fields)])
41
+ );
42
+
43
+ return Object.fromEntries(dataEntries);
44
+ };
45
+
46
+ export const createData = async (fields: FieldInfo[]): Promise<KeyValueMap> => {
47
+ const entries = await Promise.all(
48
+ fields.map(async (field) => {
49
+ const id = field?.settings?.id || 'undefined';
50
+ switch (field.settings.type) {
51
+ case FIELD_TYPE_SYMBOL:
52
+ return [id, await getSymbolFake(field)];
53
+ case FIELD_TYPE_INTEGER:
54
+ return [id, await getIntegerFake(field)];
55
+ case FIELD_TYPE_NUMBER:
56
+ return [id, await getNumberFake(field)];
57
+ case FIELD_TYPE_TEXT:
58
+ return [id, await getTextFake(field)];
59
+ case FIELD_TYPE_LINK:
60
+ return [id, await getLinkFake(field)];
61
+ case FIELD_TYPE_ARRAY:
62
+ return [id, await getArrayFake(field)];
63
+ case FIELD_TYPE_BOOLEAN:
64
+ return [id, faker.datatype.boolean()];
65
+ case FIELD_TYPE_DATE:
66
+ return [id, faker.date.future()];
67
+ case FIELD_TYPE_RICHTEXT:
68
+ return [id, await getRichtextFake(field)];
69
+ case FIELD_TYPE_OBJECT:
70
+ return [id, {}];
71
+ case FIELD_TYPE_LOCATION:
72
+ return [
73
+ id,
74
+ {
75
+ lat: faker.address.latitude(),
76
+ lng: faker.address.longitude(),
77
+ },
78
+ ];
79
+ default:
80
+ return [id, undefined];
81
+ }
82
+ })
83
+ );
84
+
85
+ return Object.fromEntries(entries);
86
+ };
87
+
88
+ type RegexType = {
89
+ pattern: string;
90
+ flags: string;
91
+ };
92
+
93
+ const getRegexValue = (regex: RegexType) => {
94
+ const { pattern, flags } = regex;
95
+
96
+ // First try to handle regex values predefined in contentful
97
+ // Email:
98
+ if (pattern === '^\\w[\\w.-]*@([\\w-]+\\.)+[\\w-]+$') {
99
+ return faker.internet.exampleEmail();
100
+ }
101
+
102
+ // Url:
103
+ if (
104
+ pattern.includes('\\/\\/(\\w+:{0,1}\\w*@)?(\\S+)(:[0-9]+)?(\\/|\\/([\\w#!:.?+=&%@!\\-/]))?$')
105
+ ) {
106
+ return faker.internet.url();
107
+ }
108
+
109
+ // Date (US)
110
+ if (pattern === '^(0?[1-9]|1[012])[- /.](0?[1-9]|[12][0-9]|3[01])[- /.](19|20)?\\d\\d$') {
111
+ return casual.time('MM-DD-YYYY');
112
+ }
113
+
114
+ // Date (European)
115
+ if (pattern === '^(0?[1-9]|[12][0-9]|3[01])[- /.](0?[1-9]|1[012])[- /.](19|20)?\\d\\d$') {
116
+ return casual.time('DD-MM-YYYY');
117
+ }
118
+
119
+ // 12h Time
120
+ if (pattern === '^(0?[1-9]|1[012]):[0-5][0-9](:[0-5][0-9])?\\s*[aApP][mM]$') {
121
+ return casual.time('hh:mm a');
122
+ }
123
+
124
+ // 24h time
125
+ if (pattern === '^(0?[0-9]|1[0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?$') {
126
+ return casual.time('HH:mm');
127
+ }
128
+
129
+ // US Phone number
130
+ if (pattern === '^\\d[ -.]?\\(?\\d\\d\\d\\)?[ -.]?\\d\\d\\d[ -.]?\\d\\d\\d\\d$') {
131
+ return faker.phone.phoneNumber();
132
+ }
133
+
134
+ // US Zip code
135
+ if (pattern === '^\\d{5}$|^\\d{5}-\\d{4}$}') {
136
+ return faker.address.zipCode();
137
+ }
138
+
139
+ return new RandExp(pattern, flags);
140
+ };
141
+
142
+ const getTextForId = (id: string) => {
143
+ if (
144
+ [
145
+ 'username',
146
+ 'first_name',
147
+ 'last_name',
148
+ 'full_name',
149
+ 'password',
150
+ 'name_prefix',
151
+ 'name_suffix',
152
+ 'company_name',
153
+ 'company_suffix',
154
+ 'catch_phrase',
155
+ 'phone',
156
+ 'ip',
157
+ 'domain',
158
+ 'url',
159
+ 'email',
160
+ 'user_agent',
161
+ 'country',
162
+ 'city',
163
+ 'zip',
164
+ 'street',
165
+ 'address',
166
+ 'address1',
167
+ 'address2',
168
+ 'state',
169
+ 'state_abbr',
170
+ 'latitude',
171
+ 'longitude',
172
+ 'building_number',
173
+ ].includes(id)
174
+ ) {
175
+ return casual?.[id]() ?? faker.lorem.word();
176
+ }
177
+
178
+ if (['uuid', 'uid', 'id'].includes(id)) {
179
+ return casual.uuid;
180
+ }
181
+
182
+ return undefined;
183
+ };
184
+
185
+ const getIntegerFake = async (field: FieldInfo): Promise<unknown> => {
186
+ return getNumberFake(field, 1);
187
+ };
188
+
189
+ const getNumberFake = async (field: FieldInfo, precision = 0.005): Promise<unknown> => {
190
+ const { validations = [] } = field.settings;
191
+
192
+ const { in: values } = validations.find((validation) => Boolean(validation?.in)) || {};
193
+ if (values) {
194
+ return oneOf(values);
195
+ }
196
+
197
+ const { range } = validations.find((validation) => Boolean(validation?.range)) || {};
198
+ if (range) {
199
+ const { min, max } = range;
200
+ if (typeof min !== 'undefined' && typeof max !== 'undefined') {
201
+ return faker.datatype.number({ min, max, precision });
202
+ }
203
+
204
+ if (typeof min !== 'undefined') {
205
+ return faker.datatype.number({ min, precision });
206
+ }
207
+
208
+ if (typeof max !== 'undefined') {
209
+ return faker.datatype.number({ max, precision });
210
+ }
211
+ }
212
+
213
+ return faker.datatype.number({ precision });
214
+ };
215
+
216
+ const getSymbolFake = async (field: FieldInfo): Promise<unknown> => {
217
+ const { widgetId } = field.interface;
218
+ const { validations = [], id } = field.settings;
219
+
220
+ const { in: values } = validations.find((validation) => Boolean(validation?.in)) || {};
221
+ if (values) {
222
+ return oneOf(values);
223
+ }
224
+
225
+ let result = '';
226
+
227
+ if (widgetId === 'urlEditor') {
228
+ result = faker.internet.url();
229
+ }
230
+
231
+ if (widgetId === 'slugEditor') {
232
+ result = faker.lorem.slug();
233
+ }
234
+
235
+ const byId = getTextForId(id);
236
+ if (!result && byId) {
237
+ return byId;
238
+ }
239
+
240
+ if (!result && ['dropdown', 'radio'].includes(widgetId)) {
241
+ result = faker.lorem.word();
242
+ }
243
+
244
+ const { regexp } = validations.find((validation) => Boolean(validation?.regexp)) || {};
245
+ if (!result && regexp) {
246
+ result = getRegexValue(regexp);
247
+ }
248
+
249
+ if (!result) {
250
+ result = faker.lorem.sentence();
251
+ }
252
+
253
+ const { size } = validations.find((validation) => Boolean(validation?.size)) || {};
254
+ if (size) {
255
+ const { min = 0, max = 255 } = size;
256
+ const count = randomInt(min, max);
257
+ result = result.slice(0, count);
258
+ }
259
+
260
+ return result;
261
+ };
262
+
263
+ const getTextFake = async (field: FieldInfo): Promise<unknown> => {
264
+ const { validations = [], id } = field.settings;
265
+ const { in: values } = validations.find((validation) => Boolean(validation?.in)) || {};
266
+
267
+ let result = '';
268
+ if (values) {
269
+ result = oneOf(values as string[]);
270
+ }
271
+
272
+ const byId = getTextForId(id);
273
+ if (!result && byId) {
274
+ result = byId;
275
+ }
276
+
277
+ const { regexp } = validations.find((validation) => Boolean(validation?.regexp)) || {};
278
+ if (!result && regexp) {
279
+ result = getRegexValue(regexp);
280
+ }
281
+
282
+ if (!result) {
283
+ result = faker.lorem.lines();
284
+ }
285
+
286
+ const { size } = validations.find((validation) => Boolean(validation?.size)) || {};
287
+ if (size) {
288
+ const { min = 0, max = 50000 } = size;
289
+ const count = randomInt(min, max);
290
+ result = result.slice(0, count);
291
+ }
292
+
293
+ return result;
294
+ };
295
+
296
+ const getAssetFake = async (): Promise<KeyValueMap> => {
297
+ const sizes = [640, 480, 600, 800, 1024];
298
+ const width = oneOf(sizes);
299
+ const height = oneOf(sizes);
300
+
301
+ return {
302
+ mime_type: 'image/jpeg',
303
+ url: faker.image.image(width, height),
304
+ title: faker.lorem.words(),
305
+ description: faker.lorem.lines(),
306
+ width,
307
+ height,
308
+ file_size: faker.datatype.number({ min: 1000, max: 200000 }),
309
+ };
310
+ };
311
+
312
+ const getEntryScheme = async (field: FieldInfo): Promise<KeyValueMap> => {
313
+ const { validations } = field.settings;
314
+
315
+ const { linkContentType = [] } =
316
+ validations.find((validation) => Boolean(validation?.linkContentType)) || {};
317
+
318
+ if (linkContentType.length > 0) {
319
+ return {
320
+ id: 'default',
321
+ content_type: oneOf(linkContentType),
322
+ };
323
+ }
324
+
325
+ return {};
326
+ };
327
+
328
+ const getLinkFake = async (field: FieldInfo): Promise<KeyValueMap> => {
329
+ if (field?.settings?.linkType === LINK_TYPE_ASSET) {
330
+ return getAssetFake();
331
+ }
332
+
333
+ return getEntryScheme(field);
334
+ };
335
+
336
+ const getRichtextFake = async (field: FieldInfo): Promise<KeyValueMap> => {
337
+ const { validations } = field.settings;
338
+
339
+ const { enabledMarks: marks = [] } =
340
+ validations.find((validation) => Boolean(validation?.enabledMarks)) || {};
341
+
342
+ const { enabledNodeTypes: nodeTypes = [] } =
343
+ validations.find((validation) => Boolean(validation?.enabledNodeTypes)) || {};
344
+
345
+ return {
346
+ node_type: 'document',
347
+ data: {},
348
+ content: [
349
+ ...nodeTypes
350
+ .filter((nodeType) => nodeType.startsWith('heading'))
351
+ .map((nodeType) => ({
352
+ node_type: nodeType,
353
+ content: [
354
+ {
355
+ node_type: 'text',
356
+ value: nodeType,
357
+ marks: [],
358
+ data: {},
359
+ },
360
+ ],
361
+ data: {},
362
+ })),
363
+ ...marks.map((mark) => ({
364
+ node_type: 'paragraph',
365
+ content: [
366
+ {
367
+ node_type: 'text',
368
+ value: mark,
369
+ marks: [mark],
370
+ data: {},
371
+ },
372
+ ],
373
+ data: {},
374
+ })),
375
+ {
376
+ node_type: 'paragraph',
377
+ content: [
378
+ {
379
+ node_type: 'text',
380
+ value: faker.lorem.sentence(),
381
+ marks: [],
382
+ data: {},
383
+ },
384
+ ],
385
+ data: {},
386
+ },
387
+ ],
388
+ };
389
+ };
390
+
391
+ const getArrayFake = async (field: FieldInfo): Promise<unknown[]> => {
392
+ const { validations } = field.settings;
393
+ const { size } = validations.find((validation) => Boolean(validation?.size)) || {};
394
+ let count = randomInt(0, 10);
395
+ if (size) {
396
+ count = randomInt(size?.min ?? 0, size?.max ?? (size?.min ?? 0) + 10);
397
+ }
398
+
399
+ if (field.settings.items.type === FIELD_TYPE_SYMBOL) {
400
+ return Promise.all(
401
+ Array(count)
402
+ .fill('')
403
+ .map(async () =>
404
+ getSymbolFake({
405
+ ...field,
406
+ settings: { ...field.settings.items, id: field.settings.id },
407
+ })
408
+ )
409
+ );
410
+ }
411
+
412
+ if (field.settings.items.type === FIELD_TYPE_TEXT) {
413
+ return Promise.all(
414
+ Array(count)
415
+ .fill('')
416
+ .map(async () =>
417
+ getTextFake({
418
+ ...field,
419
+ settings: { ...field.settings.items, id: field.settings.id },
420
+ })
421
+ )
422
+ );
423
+ }
424
+
425
+ if (
426
+ field.settings.items.type === FIELD_TYPE_LINK &&
427
+ field.settings.items.linkType === LINK_TYPE_ASSET
428
+ ) {
429
+ return Promise.all(
430
+ Array(count)
431
+ .fill('')
432
+ .map(async () => getAssetFake())
433
+ );
434
+ }
435
+
436
+ if (
437
+ field.settings.items.type === FIELD_TYPE_LINK &&
438
+ field.settings.items.linkType === LINK_TYPE_ENTRY
439
+ ) {
440
+ return Promise.all(
441
+ Array(count)
442
+ .fill('')
443
+ .map(async () =>
444
+ getEntryScheme({
445
+ ...field,
446
+ settings: { ...field.settings.items, id: field.settings.id },
447
+ })
448
+ )
449
+ );
450
+ }
451
+ };
@@ -0,0 +1,11 @@
1
+ export const oneOf = <T = string>(array: T[] = []): T => {
2
+ const index = randomInt(0, array.length);
3
+
4
+ return array[index];
5
+ };
6
+
7
+ export const randomInt = (min = 0, max = 1000000) => {
8
+ min = Math.ceil(min || 0);
9
+ max = Math.floor(max || 1000000);
10
+ return Math.floor(Math.random() * (max - min)) + min;
11
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "strict": false
6
+ },
7
+ "include": ["./src/index.ts","./src/**/*.ts"],
8
+ "exclude": ["./src/**/*.test.ts"]
9
+ }