feat(build): add block meta loader

This commit is contained in:
Sam Tolmay 2020-10-22 15:59:40 +02:00
parent cf7c3c52c5
commit 2a483ad8e6
25 changed files with 867 additions and 48 deletions

2
.gitignore vendored
View File

@ -9,4 +9,4 @@
packages/express/config/**
packages/build/src/test/writeFile.txt
packages/build/src/utils/files/writeFile.test.js
packages/build/src/test/fetchMetaCache/cachekey.json

9
.pnp.js generated
View File

@ -5026,6 +5026,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["@lowdefy/helpers", "workspace:packages/helpers"],
["@lowdefy/nunjucks", "workspace:packages/nunjucks"],
["ajv", "npm:6.12.6"],
["axios", "npm:0.20.0"],
["babel-jest", "virtual:4a7337632ff6e9ee5a1c45a62a9ff4cc325a9367b21424babda93e269fe01b671e885bc41bdeebafb83c81f2a8eebbf0102043354a4e58905f61c8c3387cda1e#npm:26.5.2"],
["babel-loader", "virtual:aad21d373d6721af7549b2798c35c4ea25d83c122770abeaa0a4343ba20d55700fd9e3c5fa3289a7e6f591d09e8790df14d525c719ca4729b178b8644cca082a#npm:8.1.0"],
["dataloader", "npm:2.0.0"],
@ -8696,6 +8697,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["is-buffer", "npm:2.0.4"]
],
"linkType": "HARD",
}],
["npm:0.20.0", {
"packageLocation": "./.yarn/cache/axios-npm-0.20.0-535d9e0086-6cf2e96317.zip/node_modules/axios/",
"packageDependencies": [
["axios", "npm:0.20.0"],
["follow-redirects", "npm:1.13.0"]
],
"linkType": "HARD",
}]
]],
["axobject-query", [

Binary file not shown.

View File

@ -31,6 +31,7 @@
"@lowdefy/helpers": "1.1.0",
"@lowdefy/nunjucks": "1.0.0",
"ajv": "6.12.6",
"axios": "0.20.0",
"dataloader": "2.0.0",
"js-yaml": "3.14.0",
"json5": "2.1.3",

View File

@ -0,0 +1,82 @@
/* eslint-disable no-console */
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import path from 'path';
import { type } from '@lowdefy/helpers';
import createFileLoader from './loaders/fileLoader';
import createFileSetter from './loaders/fileSetter';
import createMetaLoader from './loaders/metaLoader';
import buildConnections from './build/buildConnections';
import buildMenu from './build/buildMenu';
import buildPages from './build/buildPages';
import buildRefs from './build/buildRefs';
import cleanOutputDirectory from './build/cleanOutputDirectory';
import testSchema from './build/testSchema';
import writeConnections from './build/writeConnections';
import writeGlobal from './build/writeGlobal';
import writeMenus from './build/writeMenus';
import writePages from './build/writePages';
import writeRequests from './build/writeRequests';
function createContext(bootstrapContext) {
const { logger, cacheDirectory, configDirectory, outputDirectory } = bootstrapContext;
if (!type.isFunction(logger.success)) {
logger.success = logger.log;
}
const context = {
logger,
configLoader: createFileLoader({ baseDirectory: configDirectory }),
artifactSetter: createFileSetter({ baseDirectory: outputDirectory }),
outputDirectory,
cacheDirectory,
};
return context;
}
async function build() {
const bootstrapContext = {
logger: console,
cacheDirectory: path.resolve(process.cwd(), '/.lowdefy/.cache'),
configDirectory: process.cwd(),
outputDirectory: path.resolve(process.cwd(), '/.lowdefy/build'),
};
const context = createContext(bootstrapContext);
try {
let components = await buildRefs({ context });
await testSchema({ components, context });
context.metaLoader = createMetaLoader({ components, context });
await buildConnections({ components, context });
await buildPages({ components, context });
await buildMenu({ components, context });
await cleanOutputDirectory({ context });
await writeConnections({ components, context });
await writeRequests({ components, context });
await writePages({ components, context });
await writeGlobal({ components, context });
await writeMenus({ components, context });
} catch (error) {
context.logger.error(error);
throw error;
}
}
export { createContext };
export default build;

View File

@ -17,7 +17,6 @@
*/
import { set, type } from '@lowdefy/helpers';
const blockTypes = {};
/* Page and block build steps
@ -108,8 +107,8 @@ async function setBlockMeta(block, context) {
)}`
);
}
const { category, loading, module, scope, url, valueType } = meta;
block.meta = { category, loading, module, scope, url };
const { category, loading, moduleFederation, valueType } = meta;
block.meta = { category, loading, moduleFederation };
if (category === 'input') {
block.meta.valueType = valueType;
}

View File

@ -28,12 +28,14 @@ const logger = {
const blockMetas = {
Context: {
category: 'context',
scope: 'blocks',
module: 'Context',
url: 'https://example.com/remoteEntry.js',
loading: {
type: 'Spinner',
},
moduleFederation: {
scope: 'blocks',
module: 'Context',
url: 'https://example.com/remoteEntry.js',
},
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
$id: 'https://example.com/Container.json',
@ -41,12 +43,14 @@ const blockMetas = {
},
Container: {
category: 'container',
scope: 'blocks',
module: 'Container',
url: 'https://example.com/remoteEntry.js',
loading: {
type: 'Spinner',
},
moduleFederation: {
scope: 'blocks',
module: 'Container',
url: 'https://example.com/remoteEntry.js',
},
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
$id: 'https://example.com/Container.json',
@ -54,12 +58,14 @@ const blockMetas = {
},
List: {
category: 'list',
scope: 'blocks',
module: 'List',
url: 'https://example.com/remoteEntry.js',
loading: {
type: 'Spinner',
},
moduleFederation: {
scope: 'blocks',
module: 'List',
url: 'https://example.com/remoteEntry.js',
},
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
$id: 'https://example.com/Container.json',
@ -67,13 +73,15 @@ const blockMetas = {
},
Input: {
category: 'input',
scope: 'blocks',
module: 'Input',
url: 'https://example.com/remoteEntry.js',
valueType: 'string',
loading: {
type: 'SkeletonInput',
},
moduleFederation: {
scope: 'blocks',
module: 'Input',
url: 'https://example.com/remoteEntry.js',
},
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
$id: 'https://example.com/Container.json',
@ -81,12 +89,14 @@ const blockMetas = {
},
Display: {
category: 'display',
scope: 'blocks',
module: 'Display',
url: 'https://example.com/remoteEntry.js',
loading: {
type: 'Spinner',
},
moduleFederation: {
scope: 'blocks',
module: 'Display',
url: 'https://example.com/remoteEntry.js',
},
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
$id: 'https://example.com/Container.json',
@ -97,27 +107,33 @@ const blockMetas = {
const outputMetas = {
Context: {
category: 'context',
scope: 'blocks',
module: 'Context',
url: 'https://example.com/remoteEntry.js',
moduleFederation: {
scope: 'blocks',
module: 'Context',
url: 'https://example.com/remoteEntry.js',
},
loading: {
type: 'Spinner',
},
},
Container: {
category: 'container',
scope: 'blocks',
module: 'Container',
url: 'https://example.com/remoteEntry.js',
moduleFederation: {
scope: 'blocks',
module: 'Container',
url: 'https://example.com/remoteEntry.js',
},
loading: {
type: 'Spinner',
},
},
List: {
category: 'list',
scope: 'blocks',
module: 'List',
url: 'https://example.com/remoteEntry.js',
moduleFederation: {
scope: 'blocks',
module: 'List',
url: 'https://example.com/remoteEntry.js',
},
loading: {
type: 'Spinner',
},
@ -125,9 +141,11 @@ const outputMetas = {
},
Input: {
category: 'input',
scope: 'blocks',
module: 'Input',
url: 'https://example.com/remoteEntry.js',
moduleFederation: {
scope: 'blocks',
module: 'Input',
url: 'https://example.com/remoteEntry.js',
},
valueType: 'string',
loading: {
type: 'SkeletonInput',
@ -135,9 +153,11 @@ const outputMetas = {
},
Display: {
category: 'display',
scope: 'blocks',
module: 'Display',
url: 'https://example.com/remoteEntry.js',
moduleFederation: {
scope: 'blocks',
module: 'Display',
url: 'https://example.com/remoteEntry.js',
},
loading: {
type: 'Spinner',
},

View File

@ -19,9 +19,9 @@ import path from 'path';
import Dataloader from 'dataloader';
import getFile from '../utils/files/getFile';
function createFileBatchLoader({ baseDir }) {
function createFileBatchLoader({ baseDirectory }) {
async function loader(keys) {
const filePaths = keys.map((key) => path.resolve(baseDir, key));
const filePaths = keys.map((key) => path.resolve(baseDirectory, key));
const fetched = [];
const promises = filePaths.map(async (filePath) => {
const item = await getFile(filePath);

View File

@ -17,29 +17,29 @@
import path from 'path';
import createFileLoader from './fileLoader';
const baseDir = path.resolve(process.cwd(), 'src/test/fileLoader');
const baseDirectory = path.resolve(process.cwd(), 'src/test/fileLoader');
test('load file', async () => {
const fileLoader = createFileLoader({ baseDir });
const fileLoader = createFileLoader({ baseDirectory });
const res = await fileLoader.load('fileLoader1.txt');
expect(res).toEqual('File loader text file 1.');
});
test('load file, file does not exist', async () => {
const fileLoader = createFileLoader({ baseDir });
const fileLoader = createFileLoader({ baseDirectory });
const res = await fileLoader.load('doesNotExist.txt');
expect(res).toEqual(null);
});
test('load two files', async () => {
const fileLoader = createFileLoader({ baseDir });
const fileLoader = createFileLoader({ baseDirectory });
const files = ['fileLoader1.txt', 'fileLoader2.txt'];
const res = await Promise.all(files.map((file) => fileLoader.load(file)));
expect(res).toEqual(['File loader text file 1.', 'File loader text file 2.']);
});
test('load two files, one file errors', async () => {
const fileLoader = createFileLoader({ baseDir });
const fileLoader = createFileLoader({ baseDirectory });
const files = ['fileLoader1.txt', 'doesNotExist.txt'];
const res = await Promise.all(files.map((file) => fileLoader.load(file)));
expect(res).toEqual(['File loader text file 1.', null]);

View File

@ -18,12 +18,12 @@ import path from 'path';
import writeFile from '../utils/files/writeFile';
class FileSetter {
constructor({ baseDir }) {
this.baseDir = baseDir;
constructor({ baseDirectory }) {
this.baseDirectory = baseDirectory;
}
async set({ filePath, content }) {
return writeFile({ filePath: path.resolve(this.baseDir, filePath), content });
return writeFile({ filePath: path.resolve(this.baseDirectory, filePath), content });
}
}

View File

@ -18,17 +18,17 @@ import fs from 'fs';
import path from 'path';
import createFileSetter from './fileSetter';
const baseDir = path.resolve(process.cwd(), 'src/test/fileSetter');
const baseDirectory = path.resolve(process.cwd(), 'src/test/fileSetter');
test('writeFile', async () => {
const filePath = path.resolve(baseDir, 'writeFile.txt');
const filePath = path.resolve(baseDirectory, 'writeFile.txt');
try {
fs.unlinkSync(filePath);
} catch (error) {
//pass
}
expect(fs.existsSync(filePath)).toBe(false);
const fileSetter = createFileSetter({ baseDir });
const fileSetter = createFileSetter({ baseDirectory });
await fileSetter.set({
filePath: 'writeFile.txt',
content: 'Test fileSetter file',

View File

@ -0,0 +1,48 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { get } from '@lowdefy/helpers';
import Dataloader from 'dataloader';
import createGetMeta from '../utils/meta/getMeta';
function createMetaBatchLoader({ components, context }) {
const { cacheDirectory } = context;
const { types } = components;
const getMeta = createGetMeta({ cacheDirectory, types });
async function loader(keys) {
const fetched = [];
const promises = keys.map(async (key) => {
const item = await getMeta(key);
fetched.push(item);
});
await Promise.all(promises);
const returned = keys
.map((key) =>
fetched.find((item) => {
return get(item, 'type') === key;
})
)
.map((obj) => obj.meta);
return returned;
}
return loader;
}
function createMetaLoader(options) {
return new Dataloader(createMetaBatchLoader(options));
}
export default createMetaLoader;

View File

@ -0,0 +1,85 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import createGetMeta from '../utils/meta/getMeta';
import createMetaLoader from './metaLoader';
jest.mock('../utils/meta/getMeta', () => {
const mockGetMeta = jest.fn();
return () => mockGetMeta;
});
const mockGetMeta = createGetMeta();
mockGetMeta.mockImplementation((type) => {
if (type === 'Type1') {
return {
type: 'Type1',
meta: {
key: 'value1',
},
};
}
if (type === 'Type2') {
return {
type: 'Type2',
meta: {
key: 'value2',
},
};
}
throw new Error('Not found.');
});
const loaderConfig = {
components: { types: {} },
context: { cacheDirectory: 'cacheDirectory' },
};
test('load meta', async () => {
const metaLoader = createMetaLoader(loaderConfig);
const res = await metaLoader.load('Type1');
expect(res).toEqual({
key: 'value1',
});
});
test('load meta, meta does not exist', async () => {
const metaLoader = createMetaLoader(loaderConfig);
await expect(metaLoader.load('DoesNotExist')).rejects.toThrow('Not found.');
});
test('load two files', async () => {
const metaLoader = createMetaLoader(loaderConfig);
const types = ['Type1', 'Type2'];
const res = await Promise.all(types.map((type) => metaLoader.load(type)));
expect(res).toEqual([
{
key: 'value1',
},
{
key: 'value2',
},
]);
});
test('load two meta, one does not exist', async () => {
const metaLoader = createMetaLoader(loaderConfig);
const types = ['Type1', 'DoesNotExist'];
await expect(Promise.all(types.map((type) => metaLoader.load(type)))).rejects.toThrow(
'Not found.'
);
});

View File

@ -0,0 +1,39 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { type } from '@lowdefy/helpers';
function createCacheKey(location) {
if (type.isNone(location)) {
throw new Error('Failed to create cache key, location is undefined.');
}
if (!type.isString(location.url)) {
throw new Error('Location url definition should be a string.');
}
return (
location.url
// Replace all non alphanumeric characters (or _ - ) with a _
.replace(/~/g, '_tilde_')
.replace(/\^/g, '_caret_')
.replace(/\*/g, '_star_')
.replace(/[^a-zA-Z0-9-_]/g, '_')
.toLowerCase()
// Replace _json at end of the file with .json
.replace(/_json$/, '.json')
);
}
export default createCacheKey;

View File

@ -0,0 +1,77 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import createCacheKey from './createCacheKey';
test('createCacheKey converts a url to a file safe string', () => {
const location = {
url: 'https://unpkg.com/@lowdefy/blocks-antd@1.0.0/dist/Button.json',
};
expect(createCacheKey(location)).toEqual(
'https___unpkg_com__lowdefy_blocks-antd_1_0_0_dist_button.json'
);
});
test('createCacheKey replaces semantic versioning special characters', () => {
const location = {
url: '^~*',
};
expect(createCacheKey(location)).toEqual('_caret__tilde__star_');
});
test('createCacheKey semantic versioning special characters do not clash', () => {
const locationCaret = {
url: 'https://unpkg.com/@lowdefy/blocks-antd@^1.0.0/dist/button.json',
};
const locationTilde = {
url: 'https://unpkg.com/@lowdefy/blocks-antd@~1.0.0/dist/button.json',
};
expect(createCacheKey(locationCaret)).toEqual(
'https___unpkg_com__lowdefy_blocks-antd__caret_1_0_0_dist_button.json'
);
expect(createCacheKey(locationTilde)).toEqual(
'https___unpkg_com__lowdefy_blocks-antd__tilde_1_0_0_dist_button.json'
);
});
test('createCacheKey converts to lowercase', () => {
const location = {
url: 'UPPERCASE',
};
expect(createCacheKey(location)).toEqual('uppercase');
});
test('createCacheKey converts all non alphanumerics', () => {
const location = {
url: `!@#$%&()+=±§[]{};:"'\\|\`,.<>/?`,
};
expect(createCacheKey(location)).toEqual('_____________________________');
});
test('createCacheKey only replaces _json at then end of the string', () => {
const location = {
url: 'ABC$jsonDEF.jsonHIJ.json',
};
expect(createCacheKey(location)).toEqual('abc_jsondef_jsonhij.json');
});
test('createCacheKey throws if location is undefined', () => {
expect(() => createCacheKey()).toThrow('Failed to create cache key, location is undefined.');
});
test('createCacheKey throws if location url is not a string', () => {
expect(() => createCacheKey({ url: 1 })).toThrow('Location url definition should be a string.');
});

View File

@ -0,0 +1,20 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const defaultMetaLocations = {
};
export default defaultMetaLocations;

View File

@ -0,0 +1,30 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import path from 'path';
import createCacheKey from './createCacheKey';
import readFile from '../files/readFile';
function createFetchMetaCache({ cacheDirectory }) {
async function fetchMetaCache(location) {
const cacheKey = createCacheKey(location);
const fileContent = await readFile(path.resolve(cacheDirectory, cacheKey));
return JSON.parse(fileContent);
}
return fetchMetaCache;
}
export default createFetchMetaCache;

View File

@ -0,0 +1,43 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import path from 'path';
import createFetchMetaCache from './fetchMetaCache';
const cacheDirectory = path.resolve(process.cwd(), 'src/test/fetchMetaCache');
const fetchMetaCache = createFetchMetaCache({ cacheDirectory });
test('fetchMetaCache fetches from cache', async () => {
const meta = await fetchMetaCache({ url: 'cachekey.json' });
expect(meta).toEqual({
moduleFederation: {
remoteEntryUrl:
'https://unpkg.com/@lowdefy/blocks-antd@1.0.0-experimental.1/dist/remoteEntry.js',
scope: 'lowdefy_blocks_antd',
module: 'Button',
},
category: 'display',
loading: {
type: 'SkeletonButton',
},
schema: {},
});
});
test('fetchMetaCache returns null if not found', async () => {
const meta = await fetchMetaCache({ url: 'doesNotExist.json' });
expect(meta).toEqual(null);
});

View File

@ -0,0 +1,31 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import axios from 'axios';
import { type } from '@lowdefy/helpers';
async function fetchMetaUrl(location) {
if (type.isNone(location)) {
throw new Error('Failed to fetch meta, location is undefined.');
}
if (!type.isString(location.url)) {
throw new Error('Location url definition should be a string.');
}
const res = await axios.get(location.url);
return res.data;
}
export default fetchMetaUrl;

View File

@ -0,0 +1,55 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// eslint-disable-next-line no-unused-vars
import axios from 'axios';
import fetchMetaUrl from './fetchMetaUrl';
jest.mock('axios', () => {
return {
get: (url) => {
if (url === 'valid-url') {
return Promise.resolve({ data: { key: 'value' } });
}
throw new Error('Invalid url');
},
};
});
test('fetchMetaUrl fetches from url', async () => {
const meta = await fetchMetaUrl({
url: 'valid-url',
});
expect(meta).toEqual({ key: 'value' });
});
test('fetchMetaUrl request errors', async () => {
await expect(
fetchMetaUrl({
url: 'invalid-url',
})
).rejects.toThrow('Invalid url');
});
test('fetchMetaUrl throws if location is undefined', async () => {
await expect(fetchMetaUrl()).rejects.toThrow('Failed to fetch meta, location is undefined.');
});
test('fetchMetaUrl throws if location is undefined', async () => {
await expect(fetchMetaUrl({ url: 1 })).rejects.toThrow(
'Location url definition should be a string.'
);
});

View File

@ -0,0 +1,71 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
Steps to fetch meta
- check in cache
- in cache
- return
- not in cache
- fetch url
- write cache
- return
*/
import createFetchMetaCache from './fetchMetaCache';
import createWriteMetaCache from './writeMetaCache';
import defaultMetaLocations from './defaultMetaLocations';
import fetchMetaUrl from './fetchMetaUrl';
function createGetMeta({ types, cacheDirectory }) {
const metaLocations = {
...defaultMetaLocations,
...types,
};
const fetchMetaCache = createFetchMetaCache({ cacheDirectory });
const writeMetaCache = createWriteMetaCache({ cacheDirectory });
async function getMeta(type) {
if (!metaLocations[type]) {
throw new Error(
`Type ${JSON.stringify(type)} is not defined. Specify type url in types array.`
);
}
let meta;
const location = metaLocations[type];
meta = await fetchMetaCache(location);
if (meta)
return {
type,
meta,
};
meta = await fetchMetaUrl(location);
if (meta) {
await writeMetaCache({ location, meta });
return {
type,
meta,
};
}
throw new Error(
`Meta for type ${type} could not be found at location ${JSON.stringify(location)}.`
);
}
return getMeta;
}
export default createGetMeta;

View File

@ -0,0 +1,120 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import createGetMeta from './getMeta';
import createFetchMetaCache from './fetchMetaCache';
import createWriteMetaCache from './writeMetaCache';
import fetchMetaUrl from './fetchMetaUrl';
jest.mock('./fetchMetaCache', () => {
const mockFetchMetaCache = jest.fn();
return () => mockFetchMetaCache;
});
jest.mock('./writeMetaCache', () => {
const mockWriteMetaCache = jest.fn();
return () => mockWriteMetaCache;
});
jest.mock('./fetchMetaUrl', () => {
const mockFetchMetaUrl = jest.fn();
return mockFetchMetaUrl;
});
const mockFetchMetaCache = createFetchMetaCache();
const mockWriteMetaCache = createWriteMetaCache();
const mockFetchMetaUrl = fetchMetaUrl;
const types = {
Type1: {
url: 'type1Url',
},
Type2: {
url: 'type2Url',
},
};
const getMeta = createGetMeta({ types, cacheDirectory: 'cacheDirectory' });
beforeEach(() => {
mockFetchMetaCache.mockReset();
mockWriteMetaCache.mockReset();
mockFetchMetaUrl.mockReset();
});
test('getMeta cache returns from cache', async () => {
mockFetchMetaCache.mockImplementation((location) => {
if (location && location.url === 'type1Url') {
return {
key: 'value',
};
}
return null;
});
const res = await getMeta('Type1');
expect(res).toEqual({
type: 'Type1',
meta: {
key: 'value',
},
});
});
test('getMeta fetches from url and writes to cache', async () => {
mockFetchMetaUrl.mockImplementation((location) => {
if (location && location.url === 'type1Url') {
return {
key: 'value',
};
}
return null;
});
const res = await getMeta('Type1');
expect(res).toEqual({
type: 'Type1',
meta: {
key: 'value',
},
});
expect(mockWriteMetaCache.mock.calls).toEqual([
[
{
location: {
url: 'type1Url',
},
meta: {
key: 'value',
},
},
],
]);
});
test('getMeta type not in types', async () => {
await expect(getMeta('Undefined')).rejects.toThrow(
'Type "Undefined" is not defined. Specify type url in types array.'
);
});
test('getMeta undefined type', async () => {
await expect(getMeta()).rejects.toThrow(
'Type undefined is not defined. Specify type url in types array.'
);
});
test('getMeta meta not found in cache or url', async () => {
await expect(getMeta('Type2')).rejects.toThrow(
'Meta for type Type2 could not be found at location {"url":"type2Url"}.'
);
});

View File

@ -0,0 +1,32 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import path from 'path';
import createCacheKey from './createCacheKey';
import writeFile from '../files/writeFile';
function createWriteMetaCache({ cacheDirectory }) {
async function writeMetaCache({ location, meta }) {
const cacheKey = createCacheKey(location);
return writeFile({
filePath: path.resolve(cacheDirectory, cacheKey),
content: JSON.stringify(meta, null, 2),
});
}
return writeMetaCache;
}
export default createWriteMetaCache;

View File

@ -0,0 +1,47 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import fs from 'fs';
import path from 'path';
import createWriteMetaCache from './writeMetaCache';
const cacheDirectory = path.resolve(process.cwd(), 'src/test/fetchMetaCache');
const writeMetaCache = createWriteMetaCache({ cacheDirectory });
test('writeMetaCache writes to cache', async () => {
const filePath = path.resolve(cacheDirectory, 'writemetacache.json');
try {
fs.unlinkSync(filePath);
} catch (error) {
//pass
}
expect(fs.existsSync(filePath)).toBe(false);
await writeMetaCache({
location: { url: 'writemetacache.json' },
meta: {
key: 'value',
},
});
const content = fs.readFileSync(filePath, 'utf8');
expect(content).toEqual(`{
"key": "value"
}`);
try {
fs.unlinkSync(filePath);
} catch (error) {
//pass
}
});

View File

@ -3734,6 +3734,7 @@ __metadata:
"@lowdefy/helpers": 1.1.0
"@lowdefy/nunjucks": 1.0.0
ajv: 6.12.6
axios: 0.20.0
babel-jest: 26.5.2
babel-loader: 8.1.0
dataloader: 2.0.0
@ -6241,6 +6242,15 @@ __metadata:
languageName: node
linkType: hard
"axios@npm:0.20.0":
version: 0.20.0
resolution: "axios@npm:0.20.0"
dependencies:
follow-redirects: ^1.10.0
checksum: 6cf2e963176a0d18fc457d7cb71cb6cf552193619b161fc61070d9a4db3a26a87ecfe760ba36e7af174c05a6a9898dc52cfeb5c0e62e19fddd9788739553ad73
languageName: node
linkType: hard
"axios@npm:^0.18.0":
version: 0.18.1
resolution: "axios@npm:0.18.1"
@ -9835,7 +9845,7 @@ __metadata:
languageName: node
linkType: hard
"follow-redirects@npm:^1.0.0":
"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.10.0":
version: 1.13.0
resolution: "follow-redirects@npm:1.13.0"
checksum: f220828d3f153da30ea616fdbe9f6676e74e4e68c51d336a751037c1d556e2de34aa5918f58951fa19bb6517c9c88b4403a0a8bdfc40d3e779d37def2ac0f2c4