basic-implementation #1
20
.eslintrc.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"node": true,
|
||||||
|
"commonjs": true,
|
||||||
|
"es2021": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
"plugin:mocha/recommended"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 12
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint",
|
||||||
|
"mocha"
|
||||||
|
],
|
||||||
|
"rules": {}
|
||||||
|
}
|
1
.prettierrc.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{}
|
6628
package-lock.json
generated
|
@ -26,7 +26,10 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chai": "^4.2.18",
|
"@types/chai": "^4.2.18",
|
||||||
"@types/mocha": "^8.2.2",
|
"@types/mocha": "^8.2.2",
|
||||||
|
"@types/nock": "^11.1.0",
|
||||||
"@types/node": "^15.3.0",
|
"@types/node": "^15.3.0",
|
||||||
|
"@types/node-fetch": "^2.5.10",
|
||||||
|
"@types/pino": "^6.3.8",
|
||||||
"@types/sinon-chai": "^3.2.5",
|
"@types/sinon-chai": "^3.2.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.23.0",
|
"@typescript-eslint/eslint-plugin": "^4.23.0",
|
||||||
"@typescript-eslint/parser": "^4.23.0",
|
"@typescript-eslint/parser": "^4.23.0",
|
||||||
|
@ -38,16 +41,22 @@
|
||||||
"eslint-plugin-import": "^2.23.2",
|
"eslint-plugin-import": "^2.23.2",
|
||||||
"eslint-plugin-mocha": "^8.1.0",
|
"eslint-plugin-mocha": "^8.1.0",
|
||||||
"eslint-plugin-prettier": "^3.4.0",
|
"eslint-plugin-prettier": "^3.4.0",
|
||||||
|
"form-data": "^3.0.1",
|
||||||
"mocha": "^8.4.0",
|
"mocha": "^8.4.0",
|
||||||
|
"nock": "^13.0.11",
|
||||||
|
"node-fetch": "^2.6.1",
|
||||||
|
"pino": "^6.11.3",
|
||||||
"prettier": "^2.3.0",
|
"prettier": "^2.3.0",
|
||||||
"sinon-chai": "^3.6.0",
|
"sinon-chai": "^3.6.0",
|
||||||
"tmp-promise": "^3.0.2",
|
"tmp-promise": "^3.0.2",
|
||||||
|
"ts-dedent": "^2.1.1",
|
||||||
"ts-node": "^9.1.1",
|
"ts-node": "^9.1.1",
|
||||||
"ts-sinon": "^2.0.1",
|
"ts-sinon": "^2.0.1",
|
||||||
"tsc-watch": "^4.2.9",
|
"tsc-watch": "^4.2.9",
|
||||||
"typescript": "^4.2.4"
|
"typescript": "^4.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/chai-string": "^1.4.2",
|
||||||
"@types/ws": "^7.4.4"
|
"@types/ws": "^7.4.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
135
src/discordParser.ts
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
import { Client, Message, TextChannel } from "discord.js";
|
||||||
|
import { ImplementableApi } from "./implementableApi";
|
||||||
|
|
||||||
|
import { once as eventsOnce } from "events";
|
||||||
|
|
||||||
|
namespace DiscordParser {
|
||||||
|
export type Config = ImplementableApi.Config & {
|
||||||
|
token: string;
|
||||||
|
channelsId: {
|
||||||
|
idChannelYtb: string;
|
||||||
|
idChannelPeerTube: string;
|
||||||
|
};
|
||||||
|
keyWord: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ConstructorPattern = ImplementableApi.Config & {
|
||||||
|
botPrefix: string;
|
||||||
|
channels: {
|
||||||
|
ChannelYtb: TextChannel;
|
||||||
|
ChannelPeerTube: TextChannel;
|
||||||
|
};
|
||||||
|
client: Client;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChannelsType = {
|
||||||
|
ChannelYtb: TextChannel;
|
||||||
|
ChannelPeerTube: TextChannel;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TBaseDiscordMessageType = ("addListener" | "newEntriesNotify") &
|
||||||
|
ImplementableApi.TBaseMessageType;
|
||||||
|
|
||||||
|
class DiscordParser<
|
||||||
|
T extends TBaseDiscordMessageType
|
||||||
|
|||||||
|
> extends ImplementableApi<T> {
|
||||||
|
readonly botPrefix: string;
|
||||||
florent
commented
`youtubeChannel` / `peertubeChannel` comme nom
|
|||||||
|
readonly channels: {
|
||||||
|
[key: string]: TextChannel;
|
||||||
|
ChannelYtb: TextChannel;
|
||||||
|
ChannelPeerTube: TextChannel;
|
||||||
florent
commented
```ts
type Channels = {
channelYtb: TextChannel;
channelPeerTube: TextChannel;
};
```
```ts
static async instanciate(readonly config: DiscordParser.Config) {
const client = new Client();
client.login(this.token);
await eventsOnce(client, 'ready');
const channels = {
channelYtb: this.client.channels.resolve("..."),
channelPeerTube: ...
};
return new Discord({
client,
channels,
keyword
});
```
amaury.joly
commented
I made a thing like this
The problem with this is for the usage inside the router.
So I have again an async function inside my constructor, and to solved this I need to create the same pattern inside the router class. We could try maybe an events who's called at the the end of the constructor. (I took this idea from DiscordJS)
With this, the restriction is to call the event 'ready' at the end of the constrctor's tasks. It's looking more friendly than the Dependency Injection for a beginner who would to make his own ImplementableAPI. I made a thing like this
```ts
namespace DiscordParser {
/*...*/
export type ConstructorPattern = ImplementableApi.Config & {
keyWord: string;
channels: {
ChannelYtb: TextChannel;
ChannelPeerTube: TextChannel;
};
client: Client;
};
}
/*...*/
class DiscordParser extends ImplementableApi {
readonly keyWord: string;
readonly channels: {
[key: string]: TextChannel;
ChannelYtb: TextChannel;
ChannelPeerTube: TextChannel;
};
readonly client: Client;
constructor(readonly config: DiscordParser.ConstructorPattern) {
super(config);
this.keyWord = config.keyWord;
this.channels = config.channels;
this.client = config.client;
this.settingEvents();
}
static async instanciate(
config: DiscordParser.Config
): Promise<DiscordParser.ConstructorPattern> {
const client = new Client();
client.login(config.token);
await eventsOnce(client, 'ready');
const channels: ChannelsType = {
ChannelPeerTube: this.getTextChannel(
client,
config.channelsId.idChannelPeerTube
),
ChannelYtb: this.getTextChannel(
client,
config.channelsId.idChannelYtb
),
};
return {
name: config.name,
channels: channels,
client: client,
keyWord: config.keyWord,
};
}
private static getTextChannel(client: Client, id: string): TextChannel {
const channel = client.channels.resolve(id);
if (!channel || !(channel instanceof TextChannel))
throw 'Bad token or bad channel id. Be careful, the channel must be a TextChannel';
return channel;
}
/*...*/
};
```
The problem with this is for the usage inside the router.
I need to make a thing like this :
```ts
class Router {
api_array: { [key: string]: ImplementableApi } = {};
readonly routes: Router.Config;
constructor(readonly config: Router.GlobalConfig) {
this.routes = config.router;
this.api_array[config.discord.name] = new DiscordParser(
await DiscordParser.instanciate(config.discord)
);
this.api_array[config.peertubeRequester.name] = new PeerTubeRequester(
config.peertubeRequester
);
this.api_array[config.logWriter.name] = new LogWriter(config.logWriter);
}
/*...*/
}
```
So I have again an async function inside my constructor, and to solved this I need to create the same pattern inside the router class.
And by extension i need to generalize this for my ImplementableAPI class. It's included to force all the users of the ImplementableAPI to make an Dependence Injection Pattern for their API. And it's look a little bit restrictive to me.
We could try maybe an events who's called at the the end of the constructor. (I took this idea from DiscordJS)
Like that we could imagine a thing like this :
```ts
class MyClass extends ImplementableAPI {
constructor(...) {
super(...);
const promise = startAnAsynchronousTask(...);
doSomeSynchronousTask(...);
promise.then(() = > {
this.emit('ready');
});
}
private startAnAsynchronousTask(...) : Promise<void> {
await something(..);
await somethingElse();
await events.once(anObject, 'ready');
}
/*...*/
}
```
With this, the restriction is to call the event 'ready' at the end of the constrctor's tasks. It's looking more friendly than the Dependency Injection for a beginner who would to make his own ImplementableAPI.
amaury.joly
commented
Ok, my bad. I think I missunderstanded the Dependency Injection. Sleep on it. Ok, my bad. I think I missunderstanded the Dependency Injection. Sleep on it.
I'm going to try another thing.
I keep my DiscordParser Class with the instantiate method. And I 'm going to insert the created object inside an method in Router who could be named 'inject' or something in this idea. And who could take an ImplementableAPI instance in params.
|
|||||||
|
};
|
||||||
|
readonly client: Client;
|
||||||
|
|
||||||
|
constructor(readonly config: DiscordParser.ConstructorPattern) {
|
||||||
|
super(config);
|
||||||
|
this.botPrefix = config.botPrefix;
|
||||||
|
this.channels = config.channels;
|
||||||
|
this.client = config.client;
|
||||||
|
this.settingEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async instanciate<T extends TBaseDiscordMessageType>(
|
||||||
|
config: DiscordParser.Config
|
||||||
|
): Promise<DiscordParser<T>> {
|
||||||
|
const client = new Client();
|
||||||
|
client.login(config.token);
|
||||||
|
await eventsOnce(client, "ready");
|
||||||
|
const channels: ChannelsType = {
|
||||||
|
ChannelPeerTube: this.getTextChannel(
|
||||||
|
client,
|
||||||
|
config.channelsId.idChannelPeerTube
|
||||||
|
),
|
||||||
|
ChannelYtb: this.getTextChannel(client, config.channelsId.idChannelYtb),
|
||||||
|
};
|
||||||
|
|
||||||
|
return new DiscordParser<T>({
|
||||||
|
name: config.name,
|
||||||
|
channels: channels,
|
||||||
|
client: client,
|
||||||
|
botPrefix: config.keyWord,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getTextChannel(client: Client, id: string): TextChannel {
|
||||||
|
const channel = client.channels.resolve(id);
|
||||||
|
if (!channel || !(channel instanceof TextChannel))
|
||||||
|
throw "Bad token or bad channel id. Be careful, the channel must be a TextChannel";
|
||||||
amaury.joly marked this conversation as resolved
Outdated
florent
commented
```ts
if (!resp.ChannelPeerTube || !resp.ChannelYtb) {
throw new Error('Theres an issue concerned the channel check');
}
return {...};
```
|
|||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public receivedMessage(message: ImplementableApi.Message<T>) {
|
||||||
|
switch (message.type) {
|
||||||
|
case "newEntriesNotify":
|
||||||
|
this.sendMsgYtb(
|
||||||
|
`New YouTubes entries received :\n${message.rawContent.items.map(
|
||||||
|
(item: any) =>
|
||||||
|
`Author : ${item.author}\nTitle: ${item.title}\nlink: ${item.link}`
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendMsgYtb(message: string) {
|
||||||
|
const resolvedChannel = await this.channels;
|
||||||
|
resolvedChannel.ChannelYtb.send(message);
|
||||||
|
}
|
||||||
|
private async sendMsgPeerTube(message: string) {
|
||||||
|
const resolvedChannel = await this.channels;
|
||||||
|
resolvedChannel.ChannelPeerTube.send(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private settingEvents(): void {
|
||||||
|
this.client.on("message", (message: Message) => {
|
||||||
|
const resolvedChannel = this.channels;
|
||||||
|
let id_arr: string[] = [];
|
||||||
|
for (const key in this.channels) id_arr.push(resolvedChannel[key].id);
|
||||||
|
|
||||||
|
if (this.channels)
|
||||||
|
if (id_arr.includes(message.channel.id)) {
|
||||||
florent
commented
Je comprends pas la partie de ce code, il répond à quel cas d'utilisation ? Sinon :
Je comprends pas la partie de ce code, il répond à quel cas d'utilisation ?
Sinon :
```ts
if ( Object.values(this.channels).some(channel => channel.id === message.channel.id) ) {
```
|
|||||||
|
const msg_splitted = message.content.split(" ");
|
||||||
|
if (msg_splitted[0] === this.botPrefix) {
|
||||||
|
switch (msg_splitted[1]) {
|
||||||
|
case "add":
|
||||||
|
const send_message: ImplementableApi.Message<TBaseDiscordMessageType> =
|
||||||
|
{
|
||||||
|
rawContent: {
|
||||||
|
address: msg_splitted[2],
|
||||||
|
user: message.author.toString(),
|
||||||
|
date: message.createdAt.toUTCString(),
|
||||||
|
},
|
||||||
|
type: "addListener",
|
||||||
|
};
|
||||||
|
message.channel.send("Ceci est un feedback");
|
||||||
|
this.emit("addListener", send_message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DiscordParser };
|
|
@ -1,5 +1,34 @@
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
|
|
||||||
export class ImplementableApi extends EventEmitter {
|
namespace ImplementableApi {
|
||||||
|
export type Config = {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [optional] idListener is the way to identificate the listener who's the src of the newEntries
|
||||||
|
* the type field permit to know which type of message it is
|
||||||
|
* rawContent field is the string content of the message
|
||||||
|
*/
|
||||||
|
export type Message<T extends TBaseMessageType> = {
|
||||||
|
idListener?: number;
|
||||||
|
type: T | "logging";
|
||||||
|
rawContent: any;
|
||||||
|
};
|
||||||
|
export type TBaseMessageType = string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract class ImplementableApi<
|
||||||
|
T extends ImplementableApi.TBaseMessageType
|
||||||
|
> extends EventEmitter {
|
||||||
|
readonly name: string;
|
||||||
|
|
||||||
|
constructor(readonly config: ImplementableApi.Config) {
|
||||||
|
super();
|
||||||
|
this.name = config.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract receivedMessage(message: ImplementableApi.Message<T>): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ImplementableApi };
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// export * as Router from "./router"
|
// export * as Router from "./router"
|
||||||
export {Router} from "./router"
|
export { Router } from "./router";
|
||||||
|
|
||||||
export {ImplementableApi} from "./implementableApi"
|
export { ImplementableApi } from "./implementableApi";
|
||||||
|
|
43
src/logWriter.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import { ImplementableApi } from "./implementableApi";
|
||||||
|
|
||||||
|
import pino, { Logger } from "pino";
|
||||||
|
|
||||||
|
namespace LogWriter {
|
||||||
|
export type Config = ImplementableApi.Config & {
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* check nodejs buffer et throttle
|
||||||
|
*/
|
||||||
|
class LogWriter extends ImplementableApi {
|
||||||
|
readonly path: string;
|
||||||
|
readonly logger: Logger;
|
||||||
|
|
||||||
|
constructor(readonly config: LogWriter.Config) {
|
||||||
|
super(config);
|
||||||
|
this.path = config.path;
|
||||||
|
|
||||||
|
this.logger = pino(pino.destination({ dest: config.path }));
|
||||||
|
}
|
||||||
|
|
||||||
|
private writeMsg(message: string): void;
|
||||||
|
private writeMsg(message: ImplementableApi.Message): void;
|
||||||
|
private writeMsg(message: ImplementableApi.Message | string) {
|
||||||
|
if (typeof message !== "string")
|
||||||
|
message = `[${message.type}] ${
|
||||||
|
typeof message.rawContent === "string"
|
||||||
|
? message.rawContent
|
||||||
|
: JSON.stringify(message.rawContent)
|
||||||
|
} ${message.idListener ?? `( listener_id : ${message.idListener} )\n`}`;
|
||||||
|
|
||||||
|
// const str = `[${new Date().toISOString()}] ${message}\n`;
|
||||||
|
this.logger.info(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public receivedMessage(message: ImplementableApi.Message) {
|
||||||
|
this.writeMsg(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { LogWriter };
|
154
src/peertubeRequester.ts
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
import { ImplementableApi } from "./implementableApi";
|
||||||
|
// Api request lib
|
||||||
|
import fetch, { FetchError, Headers } from "node-fetch";
|
||||||
|
import { URLSearchParams } from "url";
|
||||||
|
import FormData from "form-data";
|
||||||
|
import dedent from "ts-dedent";
|
||||||
|
|
||||||
|
namespace PeerTubeRequester {
|
||||||
|
export type Config = ImplementableApi.Config & {
|
||||||
|
domain_name: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type UploadInstruction = {
|
||||||
|
[key: string]: string;
|
||||||
|
channelId: string;
|
||||||
|
targetUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ClientToken = {
|
||||||
|
client_id: string;
|
||||||
|
client_secret: string;
|
||||||
|
grant_type: "password";
|
||||||
|
response_type: "code";
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UserToken = {
|
||||||
|
access_token: string;
|
||||||
|
token_type: string;
|
||||||
|
expires_in: string;
|
||||||
|
refresh_token: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
class PeerTubeRequester extends ImplementableApi {
|
||||||
|
readonly domain_name: string;
|
||||||
|
readonly username: string;
|
||||||
|
readonly password: string;
|
||||||
|
|
||||||
|
constructor(readonly config: PeerTubeRequester.Config) {
|
||||||
|
super(config);
|
||||||
|
this.domain_name = config.domain_name;
|
||||||
|
this.username = config.username;
|
||||||
|
this.password = config.password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async receivedMessage(
|
||||||
|
message: ImplementableApi.Message
|
||||||
|
): Promise<void> {
|
||||||
|
switch (message.type) {
|
||||||
|
case "newEntriesNotify":
|
||||||
|
await this.newEntriesNotify(message);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async newEntriesNotify(
|
||||||
|
message: ImplementableApi.Message
|
||||||
|
): Promise<void> {
|
||||||
|
// parse content
|
||||||
|
const items = message.rawContent["items"];
|
||||||
|
if (Array.isArray(items))
|
||||||
|
for (const item of items) {
|
||||||
|
const media_group = item["media:group"];
|
||||||
|
const args: UploadInstruction = {
|
||||||
|
channelId: "848", // to do binding avec les idDeChaines Skeptikom
|
||||||
|
targetUrl: item.link,
|
||||||
|
};
|
||||||
|
await this.apiRequest(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async apiRequest(message: UploadInstruction): Promise<void> {
|
||||||
|
let response = await fetch(
|
||||||
|
`https://${this.domain_name}/api/v1/oauth-clients/local`
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(response.statusText); // CRASH
|
||||||
|
}
|
||||||
|
const { client_id, client_secret } = await response.json();
|
||||||
|
|
||||||
|
const client_info: { [key: string]: string } = {
|
||||||
|
client_id,
|
||||||
|
client_secret,
|
||||||
|
grant_type: "password",
|
||||||
|
response_type: "code",
|
||||||
|
username: this.username,
|
||||||
|
password: this.password,
|
||||||
|
};
|
||||||
|
|
||||||
|
let myParams = new URLSearchParams();
|
||||||
|
for (const key in client_info) myParams.append(key, client_info[key]);
|
||||||
|
|
||||||
|
response = await fetch(`https://${this.domain_name}/api/v1/users/token`, {
|
||||||
|
method: "post",
|
||||||
|
body: myParams,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(response.statusText); // CRASH
|
||||||
|
}
|
||||||
|
const { access_token } = await response.json();
|
||||||
|
|
||||||
|
// Upload
|
||||||
|
const myUploadForm = new URLSearchParams();
|
||||||
|
const myHeader = new Headers();
|
||||||
|
myHeader.append("Authorization", `Bearer ${access_token}`);
|
||||||
florent
commented
Un commentaire ici, c'est compenser à mon sens un problème sémantique dans le code. J'aurais tendance à faire deux fonctions privées Un commentaire ici, c'est compenser à mon sens un problème sémantique dans le code.
J'aurais tendance à faire deux fonctions privées `this.authenticate()` et `this.upload()`
|
|||||||
|
for (const key in message) myUploadForm.append(key, message[key]);
|
||||||
|
|
||||||
|
response = await fetch(
|
||||||
|
`https://${this.domain_name}/api/v1/videos/imports`,
|
||||||
|
{
|
||||||
|
method: "post",
|
||||||
|
headers: myHeader,
|
||||||
|
body: myUploadForm,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
switch (response.status) {
|
||||||
|
case 400:
|
||||||
|
throw new Error(
|
||||||
|
dedent`Your target URL was not accepted by the API.\
|
||||||
|
Actualy it's : ${message.targetUrl}
|
||||||
|
${response.statusText}`
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 403:
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
break;
|
||||||
|
case 409:
|
||||||
|
throw new Error(
|
||||||
|
dedent`Oops, your instance had not allowed the HTTPS import.\
|
||||||
|
Contact your administrator.
|
||||||
|
${response.statusText}`
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
dedent`Oh, you resolved an undocumented issues.\
|
||||||
|
Please report this on the git if you have the time.
|
||||||
|
ERROR: ${response.statusText}`
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { PeerTubeRequester };
|
|
@ -1,40 +1,64 @@
|
||||||
import { ImplementableApi } from "./implementableApi";
|
import { ImplementableApi } from "./implementableApi";
|
||||||
|
|
||||||
|
import EventEmitter from "events";
|
||||||
|
|
||||||
namespace Router {
|
namespace Router {
|
||||||
export type Config = {
|
export type EventsType = {
|
||||||
events: {
|
name: string;
|
||||||
"name": string,
|
type: "emit" | "received";
|
||||||
"type": 0 | 1
|
};
|
||||||
}[];
|
export type InternalConfig = {
|
||||||
routes: {
|
events: EventsType[];
|
||||||
"serviceName": string,
|
};
|
||||||
"eventAccepted": string[] | undefined,
|
|
||||||
"eventEmitted": string[] | undefined
|
|
||||||
}[]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Router {
|
class Router<
|
||||||
api_array: ImplementableApi[] = [];
|
MessageType extends ImplementableApi.TBaseMessageType
|
||||||
config: Router.Config;
|
> extends EventEmitter {
|
||||||
|
api_array: { [key: string]: ImplementableApi<MessageType> } = {};
|
||||||
|
events: Router.EventsType[];
|
||||||
|
|
||||||
constructor(readonly path: string) {
|
constructor(readonly config: Router.EventsType[]) {
|
||||||
const tmp_config: Router.Config = require(path);
|
super();
|
||||||
this.config = tmp_config;
|
this.events = config;
|
||||||
// if(tmp_config["events"] === )
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public addApi<T extends ImplementableApi>(new_api: T) {
|
public injectDependency(api: ImplementableApi<MessageType>): void {
|
||||||
const new_api_conf = this.config.routes.find((elmt) => elmt.serviceName === new_api.constructor.name);
|
if (api.name in this.api_array)
|
||||||
// test if fun exist
|
throw `The api name '${api.name}' is already take`;
|
||||||
for(let ev in new_api_conf?.eventAccepted)
|
this.api_array[api.name] = api;
|
||||||
if(ev !in new_api)
|
|
||||||
throw new Error(`The class ${new_api.constructor.name} haven't the expected function`);
|
this.setEvents(api);
|
||||||
//test if events call exist
|
|
||||||
for(let ev in new_api_conf?.eventEmitted)
|
this.receivedMessage({
|
||||||
if(ev !in new_api.eventNames)
|
rawContent: `The dependency \`${api.name}\` was well injected into the router`,
|
||||||
throw `The class ${new_api.constructor.name} haven't the well defined events`;
|
type: "logging",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setEvents(api: ImplementableApi<MessageType>) {
|
||||||
|
for (const event of this.events.filter((ev) => ev.type === "received")) {
|
||||||
|
api.on(event.name, (obj: ImplementableApi.Message<MessageType>) => {
|
||||||
|
this.receivedMessage({
|
||||||
|
type: "logging",
|
||||||
|
rawContent: `A message of type \`${obj.type}\` was emited by \`${api.name}\` with the event \`${event.name}\``,
|
||||||
|
});
|
||||||
|
this.emit(event.name, obj);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public receivedMessage(message: ImplementableApi.Message<MessageType>) {
|
||||||
|
this.redirectMessage({
|
||||||
|
rawContent: `A message of type \`${message.type}\` was received`,
|
||||||
|
type: "logging",
|
||||||
|
});
|
||||||
|
this.redirectMessage(message);
|
||||||
|
}
|
||||||
|
private redirectMessage(message: ImplementableApi.Message<MessageType>) {
|
||||||
|
for (const api in this.api_array)
|
||||||
|
this.api_array[api].receivedMessage(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Router };
|
export { Router };
|
2
tests/index-spec.d.ts
vendored
|
@ -1,2 +0,0 @@
|
||||||
export {};
|
|
||||||
//# sourceMappingURL=index-spec.d.ts.map
|
|
|
@ -1 +0,0 @@
|
||||||
{"version":3,"file":"index-spec.d.ts","sourceRoot":"","sources":["index-spec.ts"],"names":[],"mappings":""}
|
|
|
@ -1,116 +0,0 @@
|
||||||
"use strict";
|
|
||||||
var __assign = (this && this.__assign) || function () {
|
|
||||||
__assign = Object.assign || function(t) {
|
|
||||||
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
||||||
s = arguments[i];
|
|
||||||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
||||||
t[p] = s[p];
|
|
||||||
}
|
|
||||||
return t;
|
|
||||||
};
|
|
||||||
return __assign.apply(this, arguments);
|
|
||||||
};
|
|
||||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
||||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
||||||
return new (P || (P = Promise))(function (resolve, reject) {
|
|
||||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
||||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
||||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
||||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
||||||
});
|
|
||||||
};
|
|
||||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
||||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
||||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
||||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
||||||
function step(op) {
|
|
||||||
if (f) throw new TypeError("Generator is already executing.");
|
|
||||||
while (_) try {
|
|
||||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
||||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
||||||
switch (op[0]) {
|
|
||||||
case 0: case 1: t = op; break;
|
|
||||||
case 4: _.label++; return { value: op[1], done: false };
|
|
||||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
||||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
||||||
default:
|
|
||||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
||||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
||||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
||||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
||||||
if (t[2]) _.ops.pop();
|
|
||||||
_.trys.pop(); continue;
|
|
||||||
}
|
|
||||||
op = body.call(thisArg, _);
|
|
||||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
||||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
||||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
var chai_1 = __importDefault(require("chai"));
|
|
||||||
var ts_sinon_1 = __importDefault(require("ts-sinon"));
|
|
||||||
var sinon_chai_1 = __importDefault(require("sinon-chai"));
|
|
||||||
chai_1.default.use(sinon_chai_1.default);
|
|
||||||
var expect = chai_1.default.expect;
|
|
||||||
var tmp_promise_1 = require("tmp-promise");
|
|
||||||
var fs_1 = require("fs");
|
|
||||||
var index_1 = require("../src/index");
|
|
||||||
var path = require("path");
|
|
||||||
var well_build_routing_file = require(path.join(__dirname, "rsrc/wellBuildedRoutingFile.json"));
|
|
||||||
describe("testing the routing part", function () {
|
|
||||||
describe("testing the building part", function () {
|
|
||||||
it("it will test the constructor with a well format config file", function () {
|
|
||||||
//given
|
|
||||||
var fun = function () {
|
|
||||||
var r = new index_1.Router(path.join(__dirname, "rsrc/wellBuildedRoutingFile.json"));
|
|
||||||
};
|
|
||||||
var spy = ts_sinon_1.default.spy(fun);
|
|
||||||
// when
|
|
||||||
try {
|
|
||||||
spy();
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
// nothing it's a test
|
|
||||||
}
|
|
||||||
// assert
|
|
||||||
expect(spy).to.not.thrown();
|
|
||||||
});
|
|
||||||
it("it will test a bad formed file", function () {
|
|
||||||
return __awaiter(this, void 0, void 0, function () {
|
|
||||||
var _this = this;
|
|
||||||
return __generator(this, function (_a) {
|
|
||||||
switch (_a.label) {
|
|
||||||
case 0: return [4 /*yield*/, tmp_promise_1.withFile(function (file) { return __awaiter(_this, void 0, void 0, function () {
|
|
||||||
var fun, spy;
|
|
||||||
return __generator(this, function (_a) {
|
|
||||||
//given
|
|
||||||
fs_1.writeFileSync(file.path, JSON.stringify(__assign(__assign({}, well_build_routing_file), { events: 12 })));
|
|
||||||
fun = function () {
|
|
||||||
var r = new index_1.Router(file.path);
|
|
||||||
console.log(r.config);
|
|
||||||
};
|
|
||||||
spy = ts_sinon_1.default.spy(fun);
|
|
||||||
// when
|
|
||||||
try {
|
|
||||||
spy();
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
// nothing it's a test
|
|
||||||
}
|
|
||||||
// assert
|
|
||||||
expect(spy).to.not.thrown();
|
|
||||||
return [2 /*return*/];
|
|
||||||
});
|
|
||||||
}); }, { postfix: ".json" })];
|
|
||||||
case 1:
|
|
||||||
_a.sent();
|
|
||||||
return [2 /*return*/];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,59 +1,85 @@
|
||||||
import chai from "chai";
|
import chai from 'chai';
|
||||||
import sinon from "ts-sinon";
|
import sinon from 'ts-sinon';
|
||||||
import sinonChai from "sinon-chai";
|
import sinonChai from 'sinon-chai';
|
||||||
|
|
||||||
chai.use(sinonChai)
|
chai.use(sinonChai);
|
||||||
const expect = chai.expect;
|
const expect = chai.expect;
|
||||||
|
|
||||||
import { withFile } from "tmp-promise"
|
import { ImplementableApi, Router } from '../src/index';
|
||||||
import { writeFileSync } from "fs";
|
|
||||||
|
|
||||||
import { Router } from "../src/index";
|
// const well_build_routing_file: Router.GlobalConfig = require('./rsrc/wellBuildedRoutingFile.json');
|
||||||
|
|
||||||
|
const events: Router.EventsType[] = [
|
||||||
|
{
|
||||||
|
name: 'newEntries',
|
||||||
|
type: 'emit',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'addListener',
|
||||||
|
type: 'received',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
type AllowedEvents = 'newEntries' | 'addListener'; // need to match with the EventsType's names
|
||||||
|
|
||||||
const path = require("path");
|
class FakeApi extends ImplementableApi<AllowedEvents> {
|
||||||
const well_build_routing_file: Router.Config = require(path.join(__dirname, "rsrc/wellBuildedRoutingFile.json"));
|
constructor(conf: ImplementableApi.Config) {
|
||||||
|
super(conf);
|
||||||
|
}
|
||||||
|
|
||||||
|
receivedMessage(msg: ImplementableApi.Message<AllowedEvents>) {
|
||||||
|
// don't do anything
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe.only('testing the routing part', function () {
|
||||||
|
describe('testing the injection', function () {
|
||||||
|
it('will test the dependency injection', function () {
|
||||||
|
// given
|
||||||
|
const myRouter = new Router<AllowedEvents>(events);
|
||||||
|
const myObj = new FakeApi({ name: 'myFakeApi' });
|
||||||
|
// first expect
|
||||||
|
expect(myRouter.api_array).to.be.empty;
|
||||||
|
expect(myRouter.events).to.be.empty;
|
||||||
|
|
||||||
describe("testing the routing part", function () {
|
|
||||||
describe("testing the building part", function () {
|
|
||||||
it("it will test the constructor with a well format config file", function () {
|
|
||||||
//given
|
|
||||||
const fun = () => {
|
|
||||||
const r = new Router(path.join(__dirname, "rsrc/wellBuildedRoutingFile.json"));
|
|
||||||
};
|
|
||||||
const spy = sinon.spy(fun);
|
|
||||||
// when
|
// when
|
||||||
try {
|
myRouter.injectDependency(myObj);
|
||||||
spy();
|
|
||||||
} catch (error) {
|
|
||||||
// nothing it's a test
|
|
||||||
}
|
|
||||||
|
|
||||||
// assert
|
|
||||||
expect(spy).to.not.thrown();
|
|
||||||
})
|
|
||||||
|
|
||||||
it("it will test a bad formed file", async function () {
|
|
||||||
await withFile(async (file) => {
|
|
||||||
//given
|
|
||||||
writeFileSync(file.path, JSON.stringify({...well_build_routing_file, ...{events: 12}}));
|
|
||||||
const fun = () => {
|
|
||||||
const r = new Router(file.path);
|
|
||||||
console.log(r.config);
|
|
||||||
|
|
||||||
|
// expect
|
||||||
|
expect(myRouter.api_array).to.have.keys(['myFakeApi']);
|
||||||
|
expect(myRouter.api_array['myFakeApi']).to.be.instanceOf(FakeApi);
|
||||||
|
expect(myRouter.events).to.be.empty;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('testing the data transmission', function () {
|
||||||
|
it('it will emit a upload request message', function () {});
|
||||||
|
it('it will emit a new listener request', function () {});
|
||||||
|
});
|
||||||
|
describe('testing the data reception', function () {
|
||||||
|
it.only('it will received a new entries notification', function () {
|
||||||
|
//given
|
||||||
|
const myMessageWhoWouldBeSend: ImplementableApi.Message<AllowedEvents> =
|
||||||
|
{
|
||||||
|
rawContent: 'My content',
|
||||||
|
type: 'logging',
|
||||||
|
idListener: 5,
|
||||||
};
|
};
|
||||||
const spy = sinon.spy(fun);
|
const myStub = sinon.stub(FakeApi.prototype, 'receivedMessage');
|
||||||
|
const myRouter = new Router([]);
|
||||||
|
const myObj = new FakeApi({ name: 'myFakeApi' });
|
||||||
|
myRouter.injectDependency(myObj);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
try {
|
myRouter.receivedMessage(myMessageWhoWouldBeSend);
|
||||||
spy();
|
|
||||||
} catch (error) {
|
|
||||||
// nothing it's a test
|
|
||||||
}
|
|
||||||
|
|
||||||
// assert
|
// expect
|
||||||
expect(spy).to.not.thrown();
|
expect(myStub.firstCall).to.have.been.calledWith(
|
||||||
}, {postfix: ".json"});
|
myMessageWhoWouldBeSend
|
||||||
})
|
);
|
||||||
})
|
expect(myStub.secondCall).to.have.been.calledWith({
|
||||||
|
rawContent:
|
||||||
|
'The dependency `myFakeApi` was well injected into the router',
|
||||||
|
type: 'logging',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
123
tests/peertubeRequeter-spec.ts
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
import chai from 'chai';
|
||||||
|
import sinon from 'ts-sinon';
|
||||||
|
import sinonChai from 'sinon-chai';
|
||||||
|
|
||||||
|
chai.use(sinonChai);
|
||||||
|
const expect = chai.expect;
|
||||||
|
|
||||||
|
import nock, { disableNetConnect, RequestBodyMatcher } from 'nock';
|
||||||
|
|
||||||
|
import { ImplementableApi } from '../src';
|
||||||
|
import { PeerTubeRequester } from '../src/peertubeRequester';
|
||||||
|
|
||||||
|
const paramsPeerTube: PeerTubeRequester.Config = {
|
||||||
|
name: 'testedRequester',
|
||||||
|
domain_name: 'myApiAddress.yolo',
|
||||||
|
password: 'mySuperSecretPassphrase',
|
||||||
|
username: 'myUsername',
|
||||||
|
};
|
||||||
|
|
||||||
|
const newEntriesMessage: ImplementableApi.Message = {
|
||||||
|
type: 'newEntriesNotify',
|
||||||
|
rawContent: {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
author: 'channel1',
|
||||||
|
link: 'link1',
|
||||||
|
title: 'title1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
author: 'channel2',
|
||||||
|
link: 'link2',
|
||||||
|
title: 'title2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
author: 'channel3',
|
||||||
|
link: 'link3',
|
||||||
|
title: 'title3',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const UploadInstruction = {
|
||||||
|
channelId: 'undefined', //todo uncompleted test but incompleted function too
|
||||||
|
targetUrl: 'myTargerUrl',
|
||||||
|
};
|
||||||
|
|
||||||
|
// nock data
|
||||||
|
const expectedReplyOauthClient = {
|
||||||
|
client_id: 'expectedClientID',
|
||||||
|
client_secret: 'expectedClientSecret',
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedReplyTokenAddress = {
|
||||||
|
access_token: 'expectedAccessToken',
|
||||||
|
};
|
||||||
|
|
||||||
|
const bodyTokenRequest: RequestBodyMatcher = {
|
||||||
|
client_id: expectedReplyOauthClient.client_id,
|
||||||
|
client_secret: expectedReplyOauthClient.client_secret,
|
||||||
|
grant_type: 'password',
|
||||||
|
response_type: 'code',
|
||||||
|
username: paramsPeerTube.username,
|
||||||
|
password: paramsPeerTube.password,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('PeerTube Requester Test', function () {
|
||||||
|
before(function () {
|
||||||
|
disableNetConnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(function () {
|
||||||
|
nock.cleanAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('it will test the upload function', async function () {
|
||||||
|
// given
|
||||||
|
const scope = nock(`https://${paramsPeerTube.domain_name}/api/v1`)
|
||||||
|
.get(`/oauth-clients/local`)
|
||||||
|
.times(3)
|
||||||
|
.reply(200, expectedReplyOauthClient)
|
||||||
|
.post(`/users/token`, bodyTokenRequest)
|
||||||
|
.times(3)
|
||||||
|
.reply(200, expectedReplyTokenAddress);
|
||||||
|
const import_scope = nock(
|
||||||
|
`https://${paramsPeerTube.domain_name}/api/v1`
|
||||||
|
)
|
||||||
|
.matchHeader(
|
||||||
|
'authorization',
|
||||||
|
`Bearer ${expectedReplyTokenAddress.access_token}`
|
||||||
|
)
|
||||||
|
.post(`/videos/imports`, (reqBody) => {
|
||||||
|
let links: string[] = newEntriesMessage.rawContent.items.map(
|
||||||
|
(item: any) => item.link
|
||||||
|
);
|
||||||
|
|
||||||
|
const body = new URLSearchParams(reqBody);
|
||||||
|
if (body.get('channelId') === 'undefined') {
|
||||||
|
const targUrl = body.get('targetUrl');
|
||||||
|
if (targUrl && links.includes(targUrl)) {
|
||||||
|
const index = links.findIndex(
|
||||||
|
(elmt) => elmt === targUrl
|
||||||
|
);
|
||||||
|
links.splice(index, 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.times(3)
|
||||||
|
.reply(200);
|
||||||
|
|
||||||
|
const requester = new PeerTubeRequester(paramsPeerTube);
|
||||||
|
|
||||||
|
// when
|
||||||
|
await requester.receivedMessage(newEntriesMessage);
|
||||||
|
|
||||||
|
//expected
|
||||||
|
// all the scope need to be completed
|
||||||
|
expect(scope.isDone()).to.be.true;
|
||||||
|
expect(import_scope.isDone()).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,35 +1,14 @@
|
||||||
{
|
{
|
||||||
"events": [
|
"discord": {
|
||||||
{
|
"name": "Discord",
|
||||||
"name": "newEntriesNotify",
|
"token": "mysecrettoken"
|
||||||
"type": 0
|
},
|
||||||
},
|
"logWriter": {
|
||||||
{
|
"name": "logWriter",
|
||||||
"name": "uploadRequest",
|
"path": "toto.log"
|
||||||
"type": 0
|
},
|
||||||
},
|
"peertubeRequester": {
|
||||||
{
|
"name": "peertubeRequester",
|
||||||
"name": "newListenerRequest",
|
"domain_name": "fake.peertube.com"
|
||||||
"type": 0
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "removeListenerRequest",
|
|
||||||
"type": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"serviceName": "Discord",
|
|
||||||
"eventAccepted": ["newEntriesNotify"],
|
|
||||||
"eventEmitted": ["uploadRequest", "newListenerRequest", "removeListenerRequest"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"serviceName": "logWriter",
|
|
||||||
"eventAccepted": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"serviceName": "peertubeRequester",
|
|
||||||
"eventAccepted": ["uploadRequest"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
|
@ -1,4 +1,9 @@
|
||||||
{
|
{
|
||||||
"extends": "../tsconfig.json",
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"rootDir": "../",
|
||||||
|
"declarationMap": false
|
||||||
|
},
|
||||||
"include": ["./"]
|
"include": ["./"]
|
||||||
}
|
}
|
Toujours utile ?