Skip to content

Commit

Permalink
some progress on the types
Browse files Browse the repository at this point in the history
  • Loading branch information
goto-bus-stop committed Nov 2, 2021
1 parent b214f2f commit 80dd76c
Show file tree
Hide file tree
Showing 13 changed files with 160 additions and 90 deletions.
8 changes: 3 additions & 5 deletions src/Uwave.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ const migrations = require('./plugins/migrations');
const DEFAULT_MONGO_URL = 'mongodb://localhost:27017/uwave';
const DEFAULT_REDIS_URL = 'redis://localhost:6379';

/** @typedef {import('./source/Source').SourcePluginV1} SourcePluginV1 */
/** @typedef {import('./source/Source').SourcePluginV2} SourcePluginV2 */
/** @typedef {import('./source/types').StaticSourcePlugin} StaticSourcePlugin */
/** @typedef {import('./source/Source').SourceWrapper} SourceWrapper */

/**
Expand Down Expand Up @@ -228,9 +227,8 @@ class UwaveServer extends EventEmitter {
* If the first parameter is a string, returns an existing source plugin.
* Else, adds a source plugin and returns its wrapped source plugin.
*
* @typedef {((uw: UwaveServer, opts: object) => SourcePluginV1 | SourcePluginV2)}
* SourcePluginFactory
* @typedef {SourcePluginV1 | SourcePluginV2 | SourcePluginFactory} ToSourcePlugin
* @typedef {(uw: UwaveServer, opts: object) => StaticSourcePlugin} SourcePluginFactory
* @typedef {StaticSourcePlugin | SourcePluginFactory} ToSourcePlugin
*
* @param {string | Omit<ToSourcePlugin, 'default'> | { default: ToSourcePlugin }} sourcePlugin
* Source name or definition.
Expand Down
3 changes: 2 additions & 1 deletion src/controllers/import.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict';

const has = require('has');
const {
SourceNotFoundError,
SourceNoImportError,
Expand All @@ -17,7 +18,7 @@ const getImportableSource = (req) => {
if (!source) {
throw new SourceNotFoundError({ name: sourceName });
}
if (!source.import) {
if (!has(source, 'import')) {
throw new SourceNoImportError({ name: sourceName });
}

Expand Down
14 changes: 7 additions & 7 deletions src/controllers/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
const debug = require('debug')('uwave:http:search');
const { isEqual } = require('lodash');
const { SourceNotFoundError } = require('../errors');
const toListResponse = require('../utils/toListResponse');
const toPaginatedResponse = require('../utils/toPaginatedResponse');

// TODO should be deprecated once the Web client uses the better single-source route.
/**
Expand All @@ -18,7 +18,6 @@ async function searchAll(req) {
source.search(user, query).catch((error) => {
debug(error);
// Default to empty search on failure, for now.
return [];
})
));

Expand All @@ -27,7 +26,8 @@ async function searchAll(req) {
/** @type {Record<string, import('../plugins/playlists').PlaylistItemDesc[]>} */
const combinedResults = {};
sourceNames.forEach((name, index) => {
combinedResults[name] = searchResults[index];
const searchResultsForSource = searchResults[index];
combinedResults[name] = searchResultsForSource ? searchResultsForSource.data : [];
});

return combinedResults;
Expand Down Expand Up @@ -72,7 +72,7 @@ async function search(req) {
const searchResults = await source.search(user, query);

const searchResultsByID = new Map();
searchResults.forEach((result) => {
searchResults.data.forEach((result) => {
searchResultsByID.set(result.sourceID, result);
});

Expand Down Expand Up @@ -103,7 +103,7 @@ async function search(req) {
{ author: user._id },
);

searchResults.forEach((result) => {
searchResults.data.forEach((result) => {
const media = mediaBySourceID.get(String(result.sourceID));
if (media) {
// @ts-ignore
Expand All @@ -116,8 +116,8 @@ async function search(req) {
debug('sourceData update failed', error);
});

return toListResponse(searchResults, {
url: req.fullUrl,
return toPaginatedResponse(searchResults, {
baseUrl: req.fullUrl,
included: {
playlists: ['inPlaylists'],
},
Expand Down
13 changes: 7 additions & 6 deletions src/controllers/sources.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
'use strict';

const has = require('has');
const { BadRequest } = require('http-errors');
const {
SourceNotFoundError,
SourceNoImportError,
} = require('../errors');
const searchController = require('./search');
const toListResponse = require('../utils/toListResponse');
const toPaginatedResponse = require('../utils/toPaginatedResponse');

/**
* @param {import('../types').Request} req
Expand All @@ -19,7 +20,7 @@ function getImportableSource(req) {
if (!source) {
throw new SourceNotFoundError({ name: sourceName });
}
if (!source.import) {
if (has(source, 'import')) {
throw new SourceNoImportError({ name: sourceName });
}
if (source.apiVersion < 3) {
Expand All @@ -46,8 +47,8 @@ async function getPlaylists(req) {
throw new BadRequest('No playlist filter provided');
}

return toListResponse(items, {
url: req.fullUrl,
return toPaginatedResponse(items, {
baseUrl: req.fullUrl,
});
}

Expand All @@ -59,8 +60,8 @@ async function getPlaylistItems(req) {
const { playlistID } = req.params;

const items = await source.getPlaylistItems(req.user, playlistID);
return toListResponse(items, {
url: req.fullUrl,
return toPaginatedResponse(items, {
baseUrl: req.fullUrl,
});
}

Expand Down
7 changes: 7 additions & 0 deletions src/json-schema-merge-allof.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
declare module 'json-schema-merge-allof' {
import { JsonSchemaType } from 'ajv';

type Options = { deep: boolean };
declare function jsonSchemaMergeAllOf(schema: JsonSchemaType<unknown>, options?: Partial<Options>);
export = jsonSchemaMergeAllOf;
}
5 changes: 4 additions & 1 deletion src/source/ImportContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ class ImportContext extends SourceContext {
const playlist = await this.uw.playlists.createPlaylist(this.user, { name });

const rawItems = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
const items = this.source.addSourceType(rawItems);
const items = rawItems.map((item) => ({
...item,
sourceType: this.source.type,
}));

if (items.length > 0) {
await this.uw.playlists.addPlaylistItems(playlist, items);
Expand Down
86 changes: 31 additions & 55 deletions src/source/Source.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,48 +4,23 @@ const has = require('has');
const { SourceNoImportError } = require('../errors');
const SourceContext = require('./SourceContext');
const ImportContext = require('./ImportContext');
const Page = require('../Page');

/** @typedef {import('../Uwave')} Uwave */
/** @typedef {import('../models').User} User */
/** @typedef {import('../models').Playlist} Playlist */
/** @typedef {import('../plugins/playlists').PlaylistItemDesc} PlaylistItemDesc */

/**
* @typedef {object} SourceWrapper
* @prop {number} apiVersion
* @prop {(user: User, id: string) => Promise<PlaylistItemDesc | undefined>} getOne
* @prop {(user: User, ids: string[]) => Promise<PlaylistItemDesc[]>} get
* @prop {(user: User, query: string, page?: unknown) => Promise<PlaylistItemDesc[]>} search
* @prop {(user: User, userID: string) => Promise<unknown[]>} getUserPlaylists
* @prop {(user: User, playlistID: string) => Promise<PlaylistItemDesc[]>} getPlaylistItems
*/

/**
* @typedef {object} SourcePluginV1
* @prop {undefined|1} api
* @prop {(ids: string[]) => Promise<PlaylistItemDesc[]>} get
* @prop {(query: string, page: unknown, ...args: unknown[]) => Promise<PlaylistItemDesc[]>} search
*
* @typedef {object} SourcePluginV2
* @prop {2} api
* @prop {(context: SourceContext, ids: string[]) => Promise<PlaylistItemDesc[]>} get
* @prop {(
* context: SourceContext,
* query: string,
* page: unknown,
* ...args: unknown[]
* ) => Promise<PlaylistItemDesc[]>} search
* @prop {(context: ImportContext, ...args: unknown[]) => Promise<unknown>} [import]
*/
/** @typedef {import('./types').SourceWrapper} SourceWrapper */

/**
* Wrapper around source plugins with some more convenient aliases.
* @implements {SourceWrapper}
*/
class LegacySourceWrapper {
/**
* @param {Uwave} uw
* @param {string} sourceType
* @param {SourcePluginV1 | SourcePluginV2} sourcePlugin
* @param {import('./types').StaticSourcePlugin} sourcePlugin
*/
constructor(uw, sourceType, sourcePlugin) {
this.uw = uw;
Expand Down Expand Up @@ -109,32 +84,44 @@ class LegacySourceWrapper {
*
* @param {User} user
* @param {string} query
* @param {unknown} [page]
* @returns {Promise<PlaylistItemDesc[]>}
* @param {import('type-fest').JsonValue} [page]
* @returns {Promise<Page<PlaylistItemDesc, import('type-fest').JsonValue>>}
*/
async search(user, query, page) {
const context = new SourceContext(this.uw, this, user);

/** @type {PlaylistItemDesc[] | undefined} */
let results;
if (this.plugin.api === 2) {
results = await this.plugin.search(context, query, page);
} else {
results = await this.plugin.search(query, page);
}
return this.addSourceType(results);

return new Page(this.addSourceType(results), {
current: page ?? null,
});
}

/**
* Unsupported for legacy sources.
* @param {User} user
* @param {string} userID
* @returns {Promise<Page<unknown, {}>>}
*/
async getUserPlaylists() {
// eslint-disable-next-line no-unused-vars
async getUserPlaylists(user, userID) {
throw new SourceNoImportError({ name: this.type });
}

/**
* Unsupported for legacy sources.
* @param {User} user
* @param {string} playlistID
* @returns {Promise<Page<PlaylistItemDesc, {}>>}
*/
async getPlaylistItems() {
// eslint-disable-next-line no-unused-vars
async getPlaylistItems(user, playlistID) {
throw new SourceNoImportError({ name: this.type });
}

Expand All @@ -156,27 +143,13 @@ class LegacySourceWrapper {
}

/**
* @typedef {object} SourcePluginV3Statics
* @prop {3} api
* @prop {string} sourceName
* @prop {import('ajv').JSONSchemaType<unknown> & { 'uw:key': string }} schema
* @typedef {object} SourcePluginV3Instance
* @prop {(context: SourceContext, ids: string[]) => Promise<PlaylistItemDesc[]>} get
* @prop {(context: SourceContext, query: string, page: unknown) => Promise<PlaylistItemDesc[]>}
* search
* @prop {(context: SourceContext, userID: string) => Promise<unknown[]>} [getUserPlaylists]
* @prop {(context: SourceContext, sourceID: string) => Promise<PlaylistItemDesc[]>}
* [getPlaylistItems]
* @prop {() => void} [close]
* @typedef {new(options: unknown) => SourcePluginV3Instance} SourcePluginV3Constructor
* @typedef {SourcePluginV3Constructor & SourcePluginV3Statics} SourcePluginV3
* @implements {SourceWrapper}
*/

class ModernSourceWrapper {
/**
* @param {Uwave} uw
* @param {string} sourceType
* @param {SourcePluginV3Instance} sourcePlugin
* @param {import('./types').SourcePluginV3Instance<import('type-fest').JsonValue>} sourcePlugin
*/
constructor(uw, sourceType, sourcePlugin) {
this.uw = uw;
Expand Down Expand Up @@ -238,24 +211,26 @@ class ModernSourceWrapper {
*
* @param {User} user
* @param {string} query
* @param {unknown} [page]
* @returns {Promise<PlaylistItemDesc[]>}
* @param {import('type-fest').JsonValue} [page]
* @returns {Promise<Page<PlaylistItemDesc, import('type-fest').JsonValue>>}
*/
async search(user, query, page) {
const context = new SourceContext(this.uw, this, user);

const results = await this.plugin.search(context, query, page);
return this.addSourceType(results);
results.data = this.addSourceType(results.data);
return results;
}

/**
* Get playlists for a specific user from this media source.
*
* @param {User} user
* @param {string} userID
* @returns {Promise<Page<unknown, import('type-fest').JsonValue>>}
*/
async getUserPlaylists(user, userID) {
if (!has(this.plugin, 'getUserPlaylists')) {
if (!has(this.plugin, 'getUserPlaylists') || this.plugin.getUserPlaylists == null) {
throw new SourceNoImportError({ name: this.type });
}

Expand All @@ -268,9 +243,10 @@ class ModernSourceWrapper {
*
* @param {User} user
* @param {string} playlistID
* @returns {Promise<Page<PlaylistItemDesc, import('type-fest').JsonValue>>}
*/
async getPlaylistItems(user, playlistID) {
if (!has(this.plugin, 'getPlaylistItems')) {
if (!has(this.plugin, 'getPlaylistItems') || this.plugin.getPlaylistItems == null) {
throw new SourceNoImportError({ name: this.type });
}

Expand Down
3 changes: 2 additions & 1 deletion src/source/SourceContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

/** @typedef {import('../Uwave')} Uwave */
/** @typedef {import('../models').User} User */
/** @typedef {import('./types').SourceWrapper} SourceWrapper */

/**
* Data holder for things that source plugins may require.
*/
class SourceContext {
/**
* @param {Uwave} uw
* @param {Source} source
* @param {SourceWrapper} source
* @param {User} user
*/
constructor(uw, source, user) {
Expand Down
2 changes: 0 additions & 2 deletions src/source/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
'use strict';

/** @typedef {import('./Source').SourcePluginV3} SourcePluginV3} */

exports.SourceContext = require('./SourceContext');
exports.plugin = require('./plugin');
Loading

0 comments on commit 80dd76c

Please sign in to comment.