@bonniernews/fake-tool-api
Advanced tools
Comparing version 0.0.2 to 1.0.0
655
index.js
import nock from "nock"; | ||
import { randomUUID } from "crypto"; | ||
let contentByType = {}; | ||
let paths = []; | ||
const listRegex = /^\/([\w-]+)\/all(.*)$/; | ||
let types, contentByType, slugs = [], versionsMeta = {}, versions = {}, baseUrl; | ||
const listRegex = /^\/*([\w-]+)?\/all(.*)$/; | ||
const slugsRegex = /^\/slugs(\?.*)?/; | ||
const getSlugRegex = /^\/slug\/([\w-]+)/; | ||
const getSlugByValueRegex = /^\/slug\/byValue\/([\w-]+)/; | ||
const getSlugsByValuesRegex = /^\/slugs\/byValues$/; | ||
const postSlugRegex = /^\/slug(\?.*)?/; | ||
const autocompleteRegex = /^\/([\w-]+)\/autocomplete(.*)$/; | ||
const singleContentRegex = /^\/([\w-]+)\/([\w-]+)$/; | ||
const uuidRegex = | ||
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; | ||
const getPathsRegex = /^\/slug\/byValue\/([\w-]+)$/; | ||
const mgetPathsRegex = /^\/slugs\/byValues$/; | ||
const putContentRegex = /^\/([\w-]+)\/([\w-]+)\??([^&]*)$/; | ||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; | ||
const versionsRegex = /^\/([\w-]+)\/([\w-]+)\/versions$/; | ||
const versionRegex = /^\/([\w-]+)\/([\w-]+)\/versions\/\d+$/; | ||
let interceptor = () => {}; | ||
export function init(url, pubSubListenerArg) { | ||
initNock(url); | ||
initPubsub(pubSubListenerArg); | ||
resetContent(); | ||
} | ||
let interceptor = null; | ||
let pubSubListener = null; | ||
export function initPubsub(listener) { | ||
pubSubListener = listener; | ||
} | ||
let pubSubListener; | ||
export function init(toolApiBaseUrl, listener) { | ||
pubSubListener = listener; | ||
clear(); | ||
const mock = nock(toolApiBaseUrl); | ||
// Sets nock listeners and basic types (channel, publishingGroup, tag and article) | ||
export function initNock(url) { | ||
baseUrl = url; | ||
const mock = nock(url); | ||
mock | ||
.get("/types") | ||
.reply(interceptable(types)) | ||
.get(listRegex) | ||
.reply(interceptable(list)) | ||
.persist() | ||
.get(singleContentRegex) | ||
.reply(interceptable(get)) | ||
.get(slugsRegex) | ||
.reply(interceptable(slugsList)) | ||
.persist() | ||
.get(getPathsRegex) | ||
.reply(interceptable(getPaths)) | ||
.post(mgetPathsRegex) | ||
.reply(interceptable(mgetPaths)) | ||
.get(getSlugByValueRegex) | ||
.reply(interceptable(filterSlugsByValue)) | ||
.persist() | ||
.post("/slug") // todo collisionresolver | ||
.query(true) | ||
.reply(interceptable(postPath)) | ||
.post(getSlugsByValuesRegex) | ||
.reply(interceptable(filterSlugsByValues)) | ||
.persist() | ||
.put(singleContentRegex) | ||
.reply(interceptable(put)) | ||
.get(getSlugRegex) | ||
.reply(interceptable(getSlug)) | ||
.persist() | ||
.delete(getSlugRegex) | ||
.reply(interceptable(deleteSlug)) | ||
.persist() | ||
.post(postSlugRegex) | ||
.reply(interceptable(requestSlug)) | ||
.persist() | ||
.get(autocompleteRegex) | ||
.reply(interceptable(autocomplete)) | ||
.persist() | ||
.get(versionRegex) | ||
.reply(interceptable(getVersion)) | ||
.get(versionsRegex) | ||
.reply(interceptable(getVersions)) | ||
.get(singleContentRegex) | ||
.reply(interceptable(getContent)) | ||
.persist() | ||
.put(putContentRegex) | ||
.reply(interceptable(putContent)) | ||
.persist() | ||
.delete(singleContentRegex) | ||
.reply(interceptable(deleteContent)) | ||
.persist() | ||
.get("/types") | ||
.reply(interceptable(getTypes)) | ||
.persist(); | ||
@@ -46,28 +78,30 @@ } | ||
export function intercept(interceptFn) { | ||
interceptor = interceptFn; | ||
interceptor = interceptFn || (() => {}); | ||
} | ||
export async function addContent(type, id, content, skipEvents) { | ||
addType(type); | ||
const existingContent = contentByType[type][id]; | ||
const sequenceNumber = existingContent?.sequenceNumber ? existingContent?.sequenceNumber + 1 : 1; | ||
content.updated = content.updated || new Date().toISOString(); | ||
contentByType[type][id] = { sequenceNumber, content }; | ||
if (!skipEvents && pubSubListener) await sendEvent(type, id, "published"); | ||
// resets content an initialize basic types () | ||
export function resetContent() { | ||
initBasetypes(); | ||
slugs = []; | ||
interceptor = () => {}; | ||
versionsMeta = {}; | ||
versions = {}; | ||
} | ||
export function addPath(path) { | ||
if (path.channels) { | ||
throw new Error("slug.channels is deprecated, use slug.channel instead"); | ||
// Removes all base types (needed for a few very generic tests) | ||
export function clearBaseTypes() { | ||
types = {}; | ||
} | ||
export async function addContent(type, id, content, skipEvents) { | ||
if (!contentByType[type]) { | ||
contentByType[type] = {}; | ||
} | ||
if (!path.publishTime) { | ||
path.publishTime = new Date(); | ||
contentByType[type][id] = { updated: new Date().toISOString(), ...content, sequenceNumber: 1 }; | ||
if (types[type]?.versioned) { | ||
storeVersion(type, id, { ...contentByType[type][id], updated: new Date().toISOString() }); | ||
} | ||
paths.push(path); | ||
if (!skipEvents) await sendEvent(type, id, "published"); | ||
} | ||
export function removePath(path) { | ||
paths = paths.filter((p) => !(p.channel === path.channelId && p.value === path.value && p.path === path.path)); | ||
} | ||
export async function removeContent(type, id) { | ||
@@ -78,6 +112,26 @@ delete contentByType[type][id]; | ||
export function addSlug(slug) { | ||
if (!slug.publishTime) { | ||
slug.publishTime = new Date(); | ||
} | ||
slugs.push(slug); | ||
} | ||
export function removeSlug(slug) { | ||
slugs = slugs.filter((p) => !( | ||
p.channel === slug.channel | ||
&& p.value === slug.value | ||
&& p.path === slug.path)); | ||
} | ||
export function addType(type) { | ||
if (!contentByType[type]) { | ||
contentByType[type] = {}; | ||
if (!type.properties) { | ||
type.properties = {}; | ||
} | ||
if (!types[type.name]) { | ||
types[type.name] = mapType(type); | ||
} | ||
if (!contentByType[type.name]) { | ||
contentByType[type.name] = {}; | ||
} | ||
} | ||
@@ -89,55 +143,63 @@ | ||
export function peekPaths() { | ||
return paths; | ||
export function peekSlugs() { | ||
return slugs; | ||
} | ||
export function clear() { | ||
contentByType = {}; | ||
paths.length = 0; | ||
interceptor = null; | ||
} | ||
function interceptable(fn) { | ||
return function () { | ||
const interceptorFn = interceptor || (() => {}); | ||
const intercepted = interceptorFn(this.method, ...arguments); | ||
if (intercepted) return intercepted; | ||
return fn.apply(this, arguments); | ||
async function sendEvent(type, id, event) { | ||
if (!pubSubListener) return; | ||
const message = { | ||
id, | ||
data: Buffer.from(JSON.stringify({ | ||
event, | ||
type, | ||
id, | ||
updated: new Date(), | ||
})), | ||
attributes: {}, | ||
}; | ||
await pubSubListener(message); | ||
} | ||
function types() { | ||
const responseBody = Object.keys(contentByType).map((typeName) => { | ||
return { | ||
name: typeName, | ||
title: typeName, | ||
pluralTitle: typeName, | ||
}; | ||
}); | ||
return [ 200, responseBody ]; | ||
} | ||
function list(url) { | ||
const [ , type, query ] = url.match(listRegex) || []; | ||
const queryParams = new URLSearchParams(query); | ||
const parentId = queryParams.get("parent"); | ||
const ofType = contentByType[type]; | ||
if (!ofType) { | ||
return [ 404 ]; | ||
const parent = queryParams.get("parent"); | ||
let ofType; | ||
if (type) { | ||
ofType = contentByType[type]; | ||
if (!ofType) { | ||
return [ 404 ]; | ||
} | ||
} else { | ||
ofType = Object.keys(contentByType).reduce((acc, t) => { | ||
return { ...acc, ...contentByType[t] }; | ||
}, {}); | ||
} | ||
const typeByContentId = {}; | ||
for (const [ t, contentById ] of Object.entries(contentByType)) { | ||
for (const contentId of Object.keys(contentById)) { | ||
typeByContentId[contentId] = t; | ||
} | ||
} | ||
let items, nextCursor; | ||
if (parentId) { | ||
items = Object.keys(ofType) | ||
.map((id) => { | ||
if (ofType[id].attributes.parentId === parentId) { | ||
return { | ||
id, | ||
sequenceNumber: ofType[id].sequenceNumber, | ||
content: ofType[id].content, | ||
}; | ||
let items; | ||
if (parent) { | ||
if (parent === "none") { | ||
items = Object.keys(ofType).map((id) => { | ||
return { | ||
id, | ||
type: typeByContentId[id], | ||
content: ofType[id], | ||
}; | ||
}).filter(({ content }) => !content.attributes.parent); | ||
} else { | ||
items = Object.keys(ofType).map((id) => { | ||
if (ofType[id].attributes.parent === parent) { | ||
return { id, content: ofType[id], type: ofType[id].type }; | ||
} | ||
}) | ||
.filter(Boolean); | ||
}).filter(Boolean); | ||
} | ||
items.forEach((item) => { | ||
item.hasChildren = Object.values(ofType).some((potentialChild) => potentialChild.attributes?.parent === item.id); | ||
}); | ||
} else { | ||
@@ -147,4 +209,4 @@ items = Object.keys(ofType).map((id) => { | ||
id, | ||
sequenceNumber: ofType[id].sequenceNumber, | ||
content: ofType[id].content, | ||
type: typeByContentId[id], | ||
content: ofType[id], | ||
}; | ||
@@ -154,2 +216,22 @@ }); | ||
const keyword = queryParams.get("keyword"); | ||
if (keyword) { | ||
items = items.filter((item) => item.content.attributes.name.toLowerCase().includes(keyword.toLocaleLowerCase())); | ||
} | ||
const channel = queryParams.get("channel"); | ||
if (channel) { | ||
const typeDefinition = types[type]; | ||
if (typeDefinition.channelSpecific) { | ||
items = items.filter((item) => item.content.attributes.channel === channel); | ||
} else { | ||
items = items.filter((item) => item.content.attributes.channels.indexOf(channel) !== -1); | ||
} | ||
} | ||
const publishingGroup = queryParams.get("publishingGroup"); | ||
if (publishingGroup) { | ||
items = items.filter((item) => item.content.publishingGroup === publishingGroup); | ||
} | ||
const onlyPublished = queryParams.get("onlyPublished"); | ||
@@ -161,2 +243,5 @@ if (onlyPublished === "true") { | ||
} | ||
if (item.content.publishedState && item.content.publishedState !== "PUBLISHED") { | ||
return false; | ||
} | ||
return true; | ||
@@ -166,53 +251,86 @@ }); | ||
const excludeFromPublishingEvents = queryParams.get("excludeFromPublishingEvents"); | ||
if (excludeFromPublishingEvents === "true") { | ||
const excludeTypes = Object.values(types).filter((t) => t.excludeFromPublishingEvents).map((td) => td.name); | ||
items = items.filter((item) => { | ||
return excludeTypes.indexOf(item.type) === -1; | ||
}); | ||
} | ||
const filterTypes = queryParams.getAll("type"); | ||
if (filterTypes.length > 0) { | ||
items = items.filter((item) => filterTypes.includes(item.type)); | ||
} | ||
const orgLength = items.length; | ||
let responseItems = items; | ||
const from = queryParams.get("cursor"); | ||
if (from) { | ||
items = items.slice(parseInt(from)); | ||
} | ||
const size = queryParams.get("size"); | ||
if (size) { | ||
const intSize = parseInt(size); | ||
let startIndex = 0; | ||
const incomingCursor = queryParams.get("cursor"); | ||
if (incomingCursor) { | ||
const cursorItem = items.find((item) => item.id === incomingCursor); | ||
startIndex = items.indexOf(cursorItem) + 1; | ||
responseItems = items.slice(0, parseInt(size)); | ||
} | ||
let nextCursor, nextUrl; | ||
if (responseItems.length < items.length) { | ||
const reqUrl = new URL(this.req.options.href); | ||
let newCursor = parseInt(size); | ||
if (from) { | ||
newCursor += parseInt(from); | ||
} | ||
items = items.slice(startIndex, startIndex + intSize); | ||
if (items.length === intSize) { | ||
const lastItem = items.slice(-1)[0]; | ||
nextCursor = lastItem.id; | ||
if (keyword) { | ||
reqUrl.searchParams.set("keyword", keyword); | ||
} | ||
reqUrl.searchParams.set("cursor", newCursor); | ||
nextCursor = orgLength - (items.length - responseItems.length); | ||
const nextUrlObj = new URL(`${baseUrl}${url}`); | ||
nextUrlObj.searchParams.set("cursor", nextCursor); | ||
nextUrl = nextUrlObj.toString(); | ||
} | ||
const responseBody = { items, nextCursor }; | ||
responseItems = JSON.parse(JSON.stringify(responseItems)); | ||
responseItems.forEach((item) => { | ||
item.sequenceNumber = item.content.sequenceNumber; | ||
delete item.content.sequenceNumber; | ||
}); | ||
const responseBody = { items: responseItems, nextCursor, next: nextUrl }; | ||
return [ 200, responseBody ]; | ||
} | ||
function get(url) { | ||
function requestSlug(url, body) { | ||
body.channel = body.channel || body.channels[0]; | ||
body.path = body.desiredPath; | ||
delete body.desiredPath; | ||
body.id = randomUUID(); | ||
const matches = url.match(singleContentRegex); | ||
const [ , type, id ] = matches || []; | ||
if (!id.match(uuidRegex)) { | ||
if (body.publishTime === "") { | ||
return [ 400 ]; | ||
} | ||
const ofType = contentByType[type]; | ||
if (!ofType) { | ||
return [ 404 ]; | ||
} | ||
const content = ofType[id]; | ||
if (!content) { | ||
return [ 404 ]; | ||
if (!body.publishTime) { | ||
body.publishTime = new Date().toISOString(); | ||
} | ||
return [ 200, content.content, { "sequence-number": content.sequenceNumber } ]; | ||
} | ||
function getPaths(url) { | ||
const matches = url.match(getPathsRegex); | ||
const [ , id ] = matches || []; | ||
if (!id.match(uuidRegex)) { | ||
return [ 400 ]; | ||
const conflictingSlug = slugs.some((s) => s.channel === body.channel && s.path === body.path); | ||
if (conflictingSlug) { | ||
return [ 409 ]; | ||
} | ||
const matchingPaths = paths.filter((path) => path.value === id); | ||
return [ 200, { slugs: matchingPaths } ]; | ||
slugs.push(body); | ||
const responseObject = { | ||
ids: [ body.id ], | ||
path: body.path, | ||
}; | ||
return [ 200, responseObject ]; | ||
} | ||
function mgetPaths(url, body) { | ||
function filterSlugsByValue(url) { | ||
const [ , id ] = url.match(getSlugByValueRegex) || []; | ||
const filtered = slugs.filter((s) => s.value === id); | ||
return [ 200, { slugs: filtered } ]; | ||
} | ||
function filterSlugsByValues(url, body) { | ||
if (!body.values) { | ||
@@ -225,26 +343,81 @@ return [ 400 ]; | ||
}); | ||
paths.forEach((path) => { | ||
if (body.values.indexOf(path.value) !== -1) { | ||
response[path.value].push(path); | ||
slugs.forEach((slug) => { | ||
if (body.values.indexOf(slug.value) !== -1) { | ||
response[slug.value].push(slug); | ||
} | ||
}); | ||
return [ 200, response ]; | ||
} | ||
function postPath(url, body) { | ||
const { channel, value, valueType, publishTime } = body; | ||
paths.push({ | ||
path: body.desiredPath, | ||
channel, | ||
value, | ||
valueType, | ||
publishTime, | ||
}); | ||
return [ 200, "OK" ]; | ||
function getSlug(url) { | ||
const [ , id ] = url.match(getSlugRegex) || []; | ||
const slug = slugs.find((s) => s.id === id); | ||
if (!slug) { | ||
return [ 404 ]; | ||
} | ||
return [ 200, slug ]; | ||
} | ||
function put(url, body) { | ||
function deleteSlug(url) { | ||
const [ , id ] = url.match(getSlugRegex) || []; | ||
const slug = slugs.find((s) => s.id === id); | ||
if (!slug) { | ||
return [ 404 ]; | ||
} | ||
slugs.splice(slugs.indexOf(slug), 1); | ||
return [ 200 ]; | ||
} | ||
function slugsList(url) { | ||
const [ , query ] = url.match(slugsRegex) || []; | ||
const queryParams = new URLSearchParams(query); | ||
let items = slugs; | ||
const channel = queryParams.get("channel"); | ||
if (channel) { | ||
items = items.filter((item) => item.channel === channel); | ||
} | ||
const path = queryParams.get("path"); | ||
if (path) { | ||
items = items.filter((item) => item.path === path); | ||
} | ||
const valueType = queryParams.get("valueType"); | ||
if (valueType) { | ||
items = items.filter((item) => item.valueType === valueType); | ||
} | ||
const value = queryParams.get("value"); | ||
if (value) { | ||
items = items.filter((item) => item.value === value); | ||
} | ||
const responseBody = { items/* , next*/ }; | ||
return [ 200, responseBody ]; | ||
} | ||
function autocomplete(url) { | ||
const [ , type, query ] = url.match(autocompleteRegex) || []; | ||
const searchParams = new URLSearchParams(query); | ||
const q = searchParams.get("keyword"); | ||
const publishingGroup = searchParams.get("publishingGroup"); | ||
const channel = searchParams.get("channel"); | ||
const ofType = contentByType[type]; | ||
const result = Object.keys(ofType) | ||
.map((id) => { | ||
return { id, content: ofType[id] }; | ||
}) | ||
.filter(({ content }) => content.attributes.name.startsWith(q)) | ||
.filter(({ content }) => !publishingGroup || content.publishingGroup === publishingGroup) | ||
.filter(({ content }) => !channel || content.attributes.channel === channel) | ||
.map(({ id, content }) => { | ||
return { | ||
id, | ||
name: content.attributes.name, | ||
description: content.attributes.description, | ||
channel: types[type].channelSpecific ? content.attributes.channel : undefined, | ||
}; | ||
}); | ||
const responseBody = { result }; | ||
return [ 200, responseBody ]; | ||
} | ||
function getContent(url) { | ||
const matches = url.match(singleContentRegex); | ||
@@ -259,9 +432,52 @@ const [ , type, id ] = matches || []; | ||
} | ||
const content = ofType[id]; | ||
if (!content) { | ||
return [ 404 ]; | ||
} | ||
return [ 200, content, { "sequence-number": ofType[id].sequenceNumber } ]; | ||
} | ||
addContent(type, id, body); | ||
function interceptable(fn) { | ||
return function () { | ||
const intercepted = interceptor(this.method, ...arguments); | ||
if (intercepted) return intercepted; | ||
return fn.apply(this, arguments); | ||
}; | ||
return [ 200, body ]; | ||
} | ||
function deleteContent(url) { | ||
function putContent(url, body) { | ||
const matches = url.match(putContentRegex); | ||
const [ , type, id, query ] = matches || []; | ||
const qs = new URLSearchParams(query); | ||
if (!id.match(uuidRegex)) { | ||
return [ 400 ]; | ||
} | ||
const ofType = contentByType[type]; | ||
if (!ofType) { | ||
return [ 404 ]; | ||
} | ||
let parsedSequenceNumber = 0; | ||
const sequenceNumber = qs.get("ifSequenceNumber"); | ||
if (sequenceNumber !== null) { | ||
parsedSequenceNumber = parseInt(sequenceNumber); | ||
if (ofType[id] && (ofType[id].sequenceNumber !== parsedSequenceNumber)) { | ||
return [ 409 ]; | ||
} | ||
} | ||
const storedObject = JSON.parse(JSON.stringify(body)); | ||
const now = new Date().toISOString(); | ||
storedObject.updated = new Date().toISOString(); | ||
if (!ofType[id]) { | ||
storedObject.created = now; | ||
} | ||
ofType[id] = { ...storedObject, sequenceNumber: parsedSequenceNumber + 1 }; | ||
if (types[type].versioned) { | ||
storeVersion(type, id, ofType[id]); | ||
} | ||
sendEvent(type, id, "published"); | ||
return [ 200, storedObject, { "sequence-number": parsedSequenceNumber + 1 } ]; | ||
} | ||
async function deleteContent(url) { | ||
const matches = url.match(singleContentRegex); | ||
@@ -276,19 +492,150 @@ const [ , type, id ] = matches || []; | ||
} | ||
delete contentByType[type][id]; | ||
await sendEvent(type, id, "unpublished"); | ||
return [ 200 ]; | ||
} | ||
removeContent(type, id); | ||
function getTypes() { | ||
return [ 200, Object.values(types) ]; | ||
} | ||
return [ 200 ]; | ||
function initBasetypes() { | ||
contentByType = { channel: {}, "publishing-group": {} }; | ||
types = {}; | ||
addType({ | ||
name: "channel", | ||
properties: { attributes: { type: "object", properties: { name: { type: "string" } } } }, | ||
}); | ||
addType({ | ||
name: "publishing-group", | ||
properties: { attributes: { type: "object", properties: { name: { type: "string" } } } }, | ||
}); | ||
addType({ | ||
name: "article", | ||
properties: { attributes: { type: "object", properties: { headline: { type: "string" } } } }, | ||
}); | ||
} | ||
async function sendEvent(type, id, event) { | ||
const message = { | ||
id, | ||
data: Buffer.from(JSON.stringify({ | ||
event, | ||
type, | ||
id, | ||
})), | ||
attributes: {}, | ||
function mapType(type) { | ||
type.title = type.title || type.name; | ||
type.pluralTitle = type.pluralTitle || type.title; | ||
addStandardProperties(type); | ||
addTypeSpecificProperties(type); | ||
type.ui = type.ui || {}; | ||
type.ui.displayProperty = type.ui.displayProperty || "attributes.name"; | ||
return type; | ||
} | ||
function addStandardProperties(type) { | ||
type.properties.editedBy = { | ||
type: "object", | ||
properties: { | ||
name: { type: "string", required: true }, | ||
email: { type: "string", format: "email" }, | ||
oneLoginId: { type: "string" }, | ||
}, | ||
}; | ||
await pubSubListener(message); | ||
type.properties.active = { | ||
type: "boolean", | ||
description: "Archive state", | ||
required: true, | ||
}; | ||
type.properties.created = { | ||
type: "datetime", | ||
readOnly: true, | ||
}; | ||
type.properties.updated = { | ||
type: "datetime", | ||
readOnly: true, | ||
}; | ||
applyDefaults(type.properties); | ||
} | ||
function addTypeSpecificProperties(type) { | ||
if (type.hasPublishedState) { | ||
type.properties.publishedState = { | ||
description: "published state", | ||
type: "string", | ||
enum: [ "DRAFT", "FINISHED", "PUBLISHED", "CANCELED" ], | ||
required: true, | ||
}; | ||
} | ||
if (type.channelSpecific) { | ||
type.publishingGroupSpecific = true; | ||
type.properties.attributes.properties.channel = { | ||
type: "reference", | ||
referenceType: "channel", | ||
title: "Kanal", | ||
required: type.name !== "section" ? true : false, // Temp hack for backwards compatibility. Can be removed when we are master for sections. | ||
}; | ||
} | ||
if (type.publishingGroupSpecific) { | ||
type.properties.publishingGroup = { | ||
type: "reference", | ||
referenceType: "publishing-group", | ||
ui: { hidden: true }, | ||
}; | ||
} | ||
if (type.hierarchical) { | ||
type.properties.attributes.properties.parent = { | ||
type: "reference", | ||
referenceType: type.name, | ||
title: "Förälder", | ||
}; | ||
} | ||
} | ||
function applyDefaults(properties) { | ||
Object.keys(properties).forEach((propertyName) => { | ||
const property = properties[propertyName]; | ||
if (property.type === "object") { | ||
applyDefaults(property.properties); | ||
} | ||
if (property.type === "enum") { | ||
property.options.forEach((option) => { | ||
if (!option.label) { | ||
option.label = option.value; | ||
} | ||
}); | ||
} | ||
if (property.type !== "object") { | ||
property.title = property.title || propertyName; | ||
} | ||
}); | ||
} | ||
function getVersions(url) { | ||
const [ , type, id ] = url.split("/"); | ||
return [ 200, { items: versionsMeta?.[type]?.[id] } ]; | ||
} | ||
function storeVersion(type, id, content) { | ||
if (!versionsMeta[type]) { | ||
versionsMeta[type] = {}; | ||
versions[type] = {}; | ||
} | ||
if (!versionsMeta[type][id]) { | ||
versionsMeta[type][id] = []; | ||
versions[type][id] = {}; | ||
} | ||
versionsMeta[type][id].unshift({ | ||
sequenceNumber: content.sequenceNumber, | ||
created: content.updated, | ||
path: `/${type}/${id}/versions/${content.sequenceNumber}`, | ||
publishedBy: "jan.banan@example.com", | ||
}); | ||
versions[type][id][content.sequenceNumber] = content; | ||
} | ||
function getVersion(url) { | ||
const [ , type, id, , version ] = url.split("/"); | ||
return [ 200, versions[type][id][version] ]; | ||
} |
{ | ||
"name": "@bonniernews/fake-tool-api", | ||
"version": "0.0.2", | ||
"version": "1.0.0", | ||
"type": "module", | ||
@@ -5,0 +5,0 @@ "description": "Mocked Bonnier News tool api, for use in automated tests", |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
18253
571
1
1