New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@bonniernews/fake-tool-api

Package Overview
Dependencies
Maintainers
33
Versions
23
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@bonniernews/fake-tool-api - npm Package Compare versions

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",

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc