diff --git a/.vscode/launch.json b/.vscode/launch.json index 53deaf1..fac01f4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "name": "Jasmine Tests with SQLite", "env": { "MUDBASE_DB_CONFIG": "{\"client\": \"sqlite3\", \"connection\": \":memory:\"}", - "TEST_RANDOM_SEED": "0.70810" + "TEST_RANDOM_SEED": "0.15844" }, "program": "${workspaceFolder}/server/node_modules/jasmine-ts/lib/index", "args": [ diff --git a/server/db/Tag.ts b/server/db/Tag.ts index 717676e..0a80299 100644 --- a/server/db/Tag.ts +++ b/server/db/Tag.ts @@ -27,16 +27,16 @@ export async function getTagChildrenRecursive(id: number, userId: number, trx: a export async function createTag(userId: number, tag: TagWithRefs, knex: Knex): Promise { return await knex.transaction(async (trx) => { // If applicable, retrieve the parent tag. - const maybeParent: number | null = + const maybeMatches: any[] | null = tag.parentId ? (await trx.select('id') .from('tags') .where({ 'user': userId }) - .where({ 'id': tag.parentId }))[0]['id'] : + .where({ 'id': tag.parentId })) : null; // Check if the parent was found, if applicable. - if (tag.parentId && maybeParent !== tag.parentId) { + if (tag.parentId && maybeMatches && !maybeMatches.length) { throw makeNotFoundError(); } @@ -45,8 +45,8 @@ export async function createTag(userId: number, tag: TagWithRefs, knex: Knex): P name: tag.name, user: userId, }; - if (maybeParent) { - newTag['parentId'] = maybeParent; + if (tag.parentId) { + newTag['parentId'] = tag.parentId; } const tagId = (await trx('tags') .insert(newTag) diff --git a/server/test/integration/flows/ResourceFlow.ts b/server/test/integration/flows/ResourceFlow.ts index 5338a6b..fe2aa91 100644 --- a/server/test/integration/flows/ResourceFlow.ts +++ b/server/test/integration/flows/ResourceFlow.ts @@ -152,6 +152,10 @@ describe('Randomized model-based DB back-end tests', () => { let seed: string = process.env.TEST_RANDOM_SEED || Math.random().toFixed(5).toString(); console.log(`Test random seed: '${seed}'`) + // Keep track of database states. + let refState: ReferenceDatabase | undefined = undefined; + let realState: ReferenceDatabase | undefined = undefined; + try { // Create a reference DB. let refDB: ReferenceDatabase = _.cloneDeep(sampleDB); @@ -170,8 +174,8 @@ describe('Randomized model-based DB back-end tests', () => { let idMappingsRefToReal: IDMappings = importResponse; // Check that we are starting from an equal situation - let refState = normalizeDB(refDB); - let realState = normalizeDB({ + refState = normalizeDB(refDB); + realState = normalizeDB({ [1]: (await helpers.getExport(req)).body, }); expect(realState).to.deep.equal(refState); @@ -183,8 +187,11 @@ describe('Randomized model-based DB back-end tests', () => { // be generated. let dist: RandomDBActionDistribution = { type: new Map([ - [DBActionType.CreateTrack, 0.7], - [DBActionType.DeleteTrack, 0.3] + [DBActionType.CreateTrack, 0.2], + [DBActionType.CreateArtist, 0.2], + [DBActionType.CreateAlbum, 0.2], + [DBActionType.CreateTag, 0.2], + [DBActionType.DeleteTrack, 0.2] ]), userId: new Map([[1, 1.0]]), createTrackParams: { @@ -198,6 +205,38 @@ describe('Randomized model-based DB back-end tests', () => { numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]), }, }, + createAlbumParams: { + linkTracks: { + numValid: new Map([[0, 1.0]]), + numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]), + }, + linkTags: { + numValid: new Map([[0, 1.0]]), + numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]), + }, + linkArtists: { + numValid: new Map([[0, 1.0]]), + numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]), + }, + }, + createArtistParams: { + linkTracks: { + numValid: new Map([[0, 1.0]]), + numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]), + }, + linkTags: { + numValid: new Map([[0, 1.0]]), + numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]), + }, + linkAlbums: { + numValid: new Map([[0, 1.0]]), + numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]), + }, + }, + createTagParams: { + linkParent: new Map([[false, 0.45], [true, 0.45], ['nonexistent', 0.1]]), + + }, deleteTrackParams: { validTrack: new Map([[false, 0.2], [true, 0.8]]) } @@ -234,8 +273,8 @@ describe('Randomized model-based DB back-end tests', () => { expect(normalizeResponse(realResponse)).to.deep.equal(normalizeResponse(refResponse)); // Compare the database state after the action. - let refState = normalizeDB(refDB); - let realState = normalizeDB({ + refState = normalizeDB(refDB); + realState = normalizeDB({ [1]: (await helpers.getExport(req)).body, }); expect(realState).to.deep.equal(refState); @@ -246,19 +285,19 @@ describe('Randomized model-based DB back-end tests', () => { e.startingDB = normalizeDB(sampleDB); e.testSeed = seed; if (e.actual && e.expected) { - e.actualDump = tmp.tmpNameSync(); - e.expectedDump = tmp.tmpNameSync(); + e.realDBDump = tmp.tmpNameSync(); + e.refDBDump = tmp.tmpNameSync(); e.actionTraceDump = tmp.tmpNameSync(); e.startingDBDump = tmp.tmpNameSync(); - fs.writeFileSync(e.actualDump, stringify(e.actual, { space: ' ' })); - fs.writeFileSync(e.expectedDump, stringify(e.expected, { space: ' ' })); + fs.writeFileSync(e.realDBDump, stringify(realState, { space: ' ' })); + fs.writeFileSync(e.refDBDump, stringify(refState, { space: ' ' })); fs.writeFileSync(e.actionTraceDump, stringify(e.actionTrace, { space: ' ' })); fs.writeFileSync(e.startingDBDump, stringify(e.startingDB, { space: ' ' })); console.log( "A comparison error occurred. Wrote compared values to temporary files for debugging:\n" - + ` actual: ${e.actualDump}\n` - + ` expected: ${e.expectedDump}\n` + + ` actual: ${e.realDBDump}\n` + + ` expected: ${e.refDBDump}\n` + ` DB action trace: ${e.actionTraceDump}\n` + ` Starting DB: ${e.startingDBDump}\n` + ` TEST_RANDOM_SEED: ${seed}` diff --git a/server/test/integration/helpers.ts b/server/test/integration/helpers.ts index 6037aa1..0ced839 100644 --- a/server/test/integration/helpers.ts +++ b/server/test/integration/helpers.ts @@ -44,7 +44,7 @@ export async function modifyTrack( props = { name: "NewTrack" }, expectStatus: number | undefined = undefined, ) { - await req + return await req .put('/track/' + id) .send(props) .then((res: any) => { @@ -73,7 +73,7 @@ export async function checkTrack( expectStatus: number | undefined = undefined, expectResponse: any = undefined, ) { - await req + return await req .get('/track/' + id) .then((res: any) => { expectStatus && expect(res).to.have.status(expectStatus); @@ -88,7 +88,7 @@ export async function createArtist( expectStatus: number | undefined = undefined, expectResponse = undefined ) { - await req + return await req .post('/artist') .send(props) .then((res: any) => { @@ -104,7 +104,7 @@ export async function modifyArtist( props = { name: "NewArtist" }, expectStatus: number | undefined = undefined, ) { - await req + return await req .put('/artist/' + id) .send(props) .then((res: any) => { @@ -119,7 +119,7 @@ export async function checkArtist( expectStatus: number | undefined = undefined, expectResponse: any = undefined, ) { - await req + return await req .get('/artist/' + id) .then((res: any) => { expectStatus && expect(res).to.have.status(expectStatus); @@ -134,7 +134,7 @@ export async function createTag( expectStatus: number | undefined = undefined, expectResponse = undefined ) { - await req + return await req .post('/tag') .send(props) .then((res: any) => { @@ -150,7 +150,7 @@ export async function modifyTag( props = { name: "NewTag" }, expectStatus: number | undefined = undefined, ) { - await req + return await req .put('/tag/' + id) .send(props) .then((res: any) => { @@ -165,7 +165,7 @@ export async function checkTag( expectStatus: number | undefined = undefined, expectResponse: any = undefined, ) { - await req + return await req .get('/tag/' + id) .then((res: any) => { expectStatus && expect(res).to.have.status(expectStatus); @@ -180,7 +180,7 @@ export async function createAlbum( expectStatus: number | undefined = undefined, expectResponse = undefined ) { - await req + return await req .post('/album') .send(props) .then((res: any) => { @@ -196,7 +196,7 @@ export async function modifyAlbum( props = { name: "NewAlbum" }, expectStatus: number | undefined = undefined, ) { - await req + return await req .put('/album/' + id) .send(props) .then((res: any) => { @@ -211,7 +211,7 @@ export async function checkAlbum( expectStatus: number | undefined = undefined, expectResponse: any = undefined, ) { - await req + return await req .get('/album/' + id) .then((res: any) => { expectStatus && expect(res).to.have.status(expectStatus); @@ -272,7 +272,7 @@ export async function createIntegration( expectStatus: number | undefined = undefined, expectResponse = undefined ) { - await req + return await req .post('/integration') .send(props) .then((res: any) => { @@ -288,7 +288,7 @@ export async function modifyIntegration( props = { name: "NewIntegration", type: IntegrationImpl.SpotifyClientCredentials, details: {}, secretDetails: {} }, expectStatus: number | undefined = undefined, ) { - await req + return await req .put('/integration/' + id) .send(props) .then((res: any) => { @@ -303,7 +303,7 @@ export async function checkIntegration( expectStatus: number | undefined = undefined, expectResponse: any = undefined, ) { - await req + return await req .get('/integration/' + id) .then((res: any) => { expectStatus && expect(res).to.have.status(expectStatus); @@ -317,7 +317,7 @@ export async function listIntegrations( expectStatus: number | undefined = undefined, expectResponse: any = undefined, ) { - await req + return await req .get('/integration') .then((res: any) => { expectStatus && expect(res).to.have.status(expectStatus); @@ -331,7 +331,7 @@ export async function deleteIntegration( id: any, expectStatus: number | undefined = undefined, ) { - await req + return await req .delete('/integration/' + id) .then((res: any) => { expectStatus && expect(res).to.have.status(expectStatus); diff --git a/server/test/reference_model/DBReferenceModel.ts b/server/test/reference_model/DBReferenceModel.ts index 6f3224a..0174835 100644 --- a/server/test/reference_model/DBReferenceModel.ts +++ b/server/test/reference_model/DBReferenceModel.ts @@ -1,4 +1,4 @@ -import { AlbumWithRefsWithId, ArtistWithRefsWithId, DBDataFormat, PostTrackRequest, TrackWithDetails, TrackWithRefsWithId } from "../../../client/src/api/api"; +import { AlbumWithRefsWithId, ArtistWithRefsWithId, DBDataFormat, PostAlbumRequest, PostArtistRequest, PostTagRequest, PostTrackRequest, TrackWithDetails, TrackWithRefsWithId } from "../../../client/src/api/api"; import { makeNotFoundError } from "../../db/common"; import filterInPlace from "../../lib/filterInPlace"; @@ -97,6 +97,50 @@ export function createTrack(userId: number, track: PostTrackRequest, db: Referen ); } +// Create a new album. +export function createAlbum(userId: number, album: PostAlbumRequest, db: ReferenceDatabase): { id: number } { + return createObject( + userId, + album, + 'albums', + [], + [ + { field: 'artistIds', otherObjectType: 'artists' }, + { field: 'trackIds', otherObjectType: 'tracks' }, + { field: 'tagIds', otherObjectType: 'tags' }, + ], + db + ); +} + +// Create a new artist. +export function createArtist(userId: number, artist: PostArtistRequest, db: ReferenceDatabase): { id: number } { + return createObject( + userId, + artist, + 'artists', + [], + [ + { field: 'albumIds', otherObjectType: 'albums' }, + { field: 'trackIds', otherObjectType: 'tracks' }, + { field: 'tagIds', otherObjectType: 'tags' }, + ], + db + ); +} + +// Create a new tag. +export function createTag(userId: number, tag: PostTagRequest, db: ReferenceDatabase): { id: number } { + return createObject( + userId, + tag, + 'tags', + [], + [], + db + ); +} + // Delete a track. export function deleteTrack(userId: number, id: number, db: ReferenceDatabase): void { // Existence checks diff --git a/server/test/reference_model/randomGen.ts b/server/test/reference_model/randomGen.ts index d8309a9..b4f187d 100644 --- a/server/test/reference_model/randomGen.ts +++ b/server/test/reference_model/randomGen.ts @@ -1,12 +1,15 @@ -import { AlbumWithRefsWithId, ArtistWithRefsWithId, TagWithRefsWithId, TrackWithRefs } from "../../../client/src/api/api"; +import { AlbumWithRefs, AlbumWithRefsWithId, ArtistWithRefs, ArtistWithRefsWithId, TagWithRefs, TagWithRefsWithId, TrackWithRefs, TrackWithRefsWithId } from "../../../client/src/api/api"; import { userEndpoints } from "../../endpoints/User"; -import { createTrack, deleteTrack, ReferenceDatabase } from "./DBReferenceModel"; +import { createAlbum, createArtist, createTag, createTrack, deleteTrack, ReferenceDatabase } from "./DBReferenceModel"; import * as helpers from '../integration/helpers'; import { DBErrorKind, isDBError } from "../../endpoints/types"; export enum DBActionType { - CreateTrack = 0, - DeleteTrack, + CreateTrack = "CreateTrack", + CreateAlbum = "CreateAlbum", + CreateTag = "CreateTag", + CreateArtist = "CreateArtist", + DeleteTrack = "DeleteTrack", } export interface DBAction { @@ -21,6 +24,9 @@ export interface RandomDBActionDistribution { type: Distribution, userId: Distribution, createTrackParams: RandomCreateTrackDistribution, + createAlbumParams: RandomCreateAlbumDistribution, + createArtistParams: RandomCreateArtistDistribution, + createTagParams: RandomCreateTagDistribution, deleteTrackParams: RandomDeleteTrackDistribution, } @@ -36,6 +42,40 @@ export interface RandomCreateTrackDistribution { linkAlbum: Distribution, } +export interface RandomCreateAlbumDistribution { + linkArtists: { + numValid: Distribution, + numInvalid: Distribution, + } + linkTags: { + numValid: Distribution, + numInvalid: Distribution, + } + linkTracks: { + numValid: Distribution, + numInvalid: Distribution, + } +} + +export interface RandomCreateArtistDistribution { + linkAlbums: { + numValid: Distribution, + numInvalid: Distribution, + } + linkTags: { + numValid: Distribution, + numInvalid: Distribution, + } + linkTracks: { + numValid: Distribution, + numInvalid: Distribution, + } +} + +export interface RandomCreateTagDistribution { + linkParent: Distribution, +} + export interface RandomDeleteTrackDistribution { validTrack: Distribution, } @@ -90,12 +130,25 @@ export function applyReferenceDBAction( let response: any = undefined; let status: number = 0; - console.log('ref action:', action); try { switch (action.type) { case DBActionType.CreateTrack: { response = createTrack(action.userId, action.payload, db); - console.log("create track ref response", response) + status = 200; + break; + } + case DBActionType.CreateAlbum: { + response = createAlbum(action.userId, action.payload, db); + status = 200; + break; + } + case DBActionType.CreateArtist: { + response = createArtist(action.userId, action.payload, db); + status = 200; + break; + } + case DBActionType.CreateTag: { + response = createTag(action.userId, action.payload, db); status = 200; break; } @@ -137,6 +190,24 @@ export async function applyRealDBAction( response = res.body; break; } + case DBActionType.CreateAlbum: { + let res = await helpers.createAlbum(req, action.payload); + status = res.status; + response = res.body; + break; + } + case DBActionType.CreateArtist: { + let res = await helpers.createArtist(req, action.payload); + status = res.status; + response = res.body; + break; + } + case DBActionType.CreateTag: { + let res = await helpers.createTag(req, action.payload); + status = res.status; + response = res.body; + break; + } case DBActionType.DeleteTrack: { let res = await helpers.deleteTrack(req, action.payload); status = res.status; @@ -175,6 +246,42 @@ export function randomDBAction( userId: userId, }; } + case DBActionType.CreateArtist: { + return { + type: type, + payload: createRandomArtist( + db, + userId, + distribution.createArtistParams, + randomNumGen + ), + userId: userId, + }; + } + case DBActionType.CreateAlbum: { + return { + type: type, + payload: createRandomAlbum( + db, + userId, + distribution.createAlbumParams, + randomNumGen + ), + userId: userId, + }; + } + case DBActionType.CreateTag: { + return { + type: type, + payload: createRandomTag( + db, + userId, + distribution.createTagParams, + randomNumGen + ), + userId: userId, + }; + } case DBActionType.DeleteTrack: { return { type: type, @@ -187,48 +294,72 @@ export function randomDBAction( } } -export function createRandomTrack( - db: ReferenceDatabase, - userId: number, - trackDist: RandomCreateTrackDistribution, - randomNumGen: any, -): TrackWithRefs { +function pickArtists(userId: number, nValid: number, nInvalid: number, rng: any, db: ReferenceDatabase) { let allValidArtistIds: number[] = db[userId] && db[userId].artists ? db[userId].artists.map((a: ArtistWithRefsWithId) => a.id) : []; - let allValidTagIds: number[] = db[userId] && db[userId].tags ? - db[userId].tags.map((a: TagWithRefsWithId) => a.id) : []; + let validArtists: number[] = pickNFromArray(allValidArtistIds, rng, nValid); + let invalidArtists: number[] = []; + for (let i = 0; i < nInvalid; i++) { + invalidArtists.push(Math.round(rng() * 100) + allValidArtistIds.length); + } + return [...validArtists, ...invalidArtists]; +} + +function pickAlbums(userId: number, nValid: number, nInvalid: number, rng: any, db: ReferenceDatabase) { let allValidAlbumIds: number[] = db[userId] && db[userId].albums ? db[userId].albums.map((a: AlbumWithRefsWithId) => a.id) : []; + let validAlbums: number[] = pickNFromArray(allValidAlbumIds, rng, nValid); + let invalidAlbums: number[] = []; + for (let i = 0; i < nInvalid; i++) { + invalidAlbums.push(Math.round(rng() * 100) + allValidAlbumIds.length); + } + return [...validAlbums, ...invalidAlbums]; +} - let artists: number[] = (() => { - let validArtists: number[] = pickNFromArray(allValidArtistIds, randomNumGen, applyDistribution(trackDist.linkArtists.numValid, randomNumGen)); - let invalidArtists: number[] = []; - for (let i = 0; i < applyDistribution(trackDist.linkArtists.numInvalid, randomNumGen); i++) { - invalidArtists.push(Math.round(randomNumGen() * 100) + allValidArtistIds.length); - } - return [...validArtists, ...invalidArtists]; - })(); +function pickTracks(userId: number, nValid: number, nInvalid: number, rng: any, db: ReferenceDatabase) { + let allValidTrackIds: number[] = db[userId] && db[userId].tracks ? + db[userId].tracks.map((a: TrackWithRefsWithId) => a.id) : []; + let validTracks: number[] = pickNFromArray(allValidTrackIds, rng, nValid); + let invalidTracks: number[] = []; + for (let i = 0; i < nInvalid; i++) { + invalidTracks.push(Math.round(rng() * 100) + allValidTrackIds.length); + } + return [...validTracks, ...invalidTracks]; +} - let tags: number[] = (() => { - let validTags: number[] = pickNFromArray(allValidTagIds, randomNumGen, applyDistribution(trackDist.linkTags.numValid, randomNumGen)); - let invalidTags: number[] = []; - for (let i = 0; i < applyDistribution(trackDist.linkTags.numInvalid, randomNumGen); i++) { - invalidTags.push(Math.round(randomNumGen() * 100) + allValidTagIds.length); - } - return [...validTags, ...invalidTags]; - })(); +function pickTags(userId: number, nValid: number, nInvalid: number, rng: any, db: ReferenceDatabase) { + let allValidTagIds: number[] = db[userId] && db[userId].tags ? + db[userId].tags.map((a: TagWithRefsWithId) => a.id) : []; + let validTags: number[] = pickNFromArray(allValidTagIds, rng, nValid); + let invalidTags: number[] = []; + for (let i = 0; i < nInvalid; i++) { + invalidTags.push(Math.round(rng() * 100) + allValidTagIds.length); + } + return [...validTags, ...invalidTags]; +} + +export function createRandomTrack( + db: ReferenceDatabase, + userId: number, + dist: RandomCreateTrackDistribution, + randomNumGen: any, +): TrackWithRefs { + let artists: number[] = pickArtists(userId, + applyDistribution(dist.linkArtists.numValid, randomNumGen), + applyDistribution(dist.linkArtists.numInvalid, randomNumGen), + randomNumGen, + db); + + let tags: number[] = pickTags(userId, + applyDistribution(dist.linkTags.numValid, randomNumGen), + applyDistribution(dist.linkTags.numInvalid, randomNumGen), + randomNumGen, + db); let maybeAlbum: number | null = (() => { - let r: boolean | null | 'nonexistent' = applyDistribution(trackDist.linkAlbum, randomNumGen); - let maybeValidAlbum: number | null = - r === true && - allValidAlbumIds.length ? - pickNFromArray(allValidAlbumIds, randomNumGen, 1)[0] : - null; - let maybeInvalidAlbum: number | null = - r === 'nonexistent' ? - allValidAlbumIds.length + 1 : null; - return maybeValidAlbum || maybeInvalidAlbum; + let r: boolean | null | 'nonexistent' = applyDistribution(dist.linkAlbum, randomNumGen); + let albums = pickAlbums(userId, r === true ? 1 : 0, r === 'nonexistent' ? 1 : 0, randomNumGen, db); + return albums.length ? albums[0] : null; })(); return { @@ -239,4 +370,91 @@ export function createRandomTrack( name: randomString(randomNumGen, 20), storeLinks: [], // TODO } +} + +export function createRandomArtist( + db: ReferenceDatabase, + userId: number, + dist: RandomCreateArtistDistribution, + randomNumGen: any, +): ArtistWithRefs { + let tracks: number[] = pickTracks(userId, + applyDistribution(dist.linkTracks.numValid, randomNumGen), + applyDistribution(dist.linkTracks.numInvalid, randomNumGen), + randomNumGen, + db); + + let tags: number[] = pickTags(userId, + applyDistribution(dist.linkTags.numValid, randomNumGen), + applyDistribution(dist.linkTags.numInvalid, randomNumGen), + randomNumGen, + db); + + let albums: number[] = pickAlbums(userId, + applyDistribution(dist.linkAlbums.numValid, randomNumGen), + applyDistribution(dist.linkAlbums.numInvalid, randomNumGen), + randomNumGen, + db); + + return { + mbApi_typename: 'artist', + albumIds: albums, + trackIds: tracks, + tagIds: tags, + name: randomString(randomNumGen, 20), + storeLinks: [], // TODO + } +} + +export function createRandomAlbum( + db: ReferenceDatabase, + userId: number, + dist: RandomCreateAlbumDistribution, + randomNumGen: any, +): AlbumWithRefs { + let tracks: number[] = pickTracks(userId, + applyDistribution(dist.linkTracks.numValid, randomNumGen), + applyDistribution(dist.linkTracks.numInvalid, randomNumGen), + randomNumGen, + db); + + let tags: number[] = pickTags(userId, + applyDistribution(dist.linkTags.numValid, randomNumGen), + applyDistribution(dist.linkTags.numInvalid, randomNumGen), + randomNumGen, + db); + + let artists: number[] = pickArtists(userId, + applyDistribution(dist.linkArtists.numValid, randomNumGen), + applyDistribution(dist.linkArtists.numInvalid, randomNumGen), + randomNumGen, + db); + + return { + mbApi_typename: 'album', + artistIds: artists, + trackIds: tracks, + tagIds: tags, + name: randomString(randomNumGen, 20), + storeLinks: [], // TODO + } +} + +export function createRandomTag( + db: ReferenceDatabase, + userId: number, + dist: RandomCreateTagDistribution, + randomNumGen: any, +): TagWithRefs { + let maybeParent: number | null = (() => { + let r: boolean | null | 'nonexistent' = applyDistribution(dist.linkParent, randomNumGen); + let tags = pickTags(userId, r === true ? 1 : 0, r === 'nonexistent' ? 1 : 0, randomNumGen, db); + return tags.length ? tags[0] : null; + })(); + + return { + mbApi_typename: 'tag', + name: randomString(randomNumGen, 20), + parentId: maybeParent, + } } \ No newline at end of file