mirror of
https://github.com/autismjs/monorepo.git
synced 2026-01-09 12:47:58 -05:00
add basic group api
This commit is contained in:
782
package-lock.json
generated
782
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -26,11 +26,13 @@
|
||||
"async-mutex": "0.4.0",
|
||||
"eventemitter2": "^6.4.9",
|
||||
"node-cache": "^5.1.2",
|
||||
"sequelize": "^6.35.2",
|
||||
"pg": "^8.11.3",
|
||||
"pg-native": "^3.0.1",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/yargs": "^17.0.32",
|
||||
"@types/pg": "^8.10.9",
|
||||
"nodemon": "^3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import GroupRegistry from '../src';
|
||||
|
||||
(async () => {
|
||||
const registry = new GroupRegistry();
|
||||
const registry = new GroupRegistry({
|
||||
user: 'postgres',
|
||||
password: 'password',
|
||||
host: 'localhost',
|
||||
pgPort: 5432,
|
||||
database: 'group-dev',
|
||||
max: 10,
|
||||
idleTimeoutMillis: 30000,
|
||||
});
|
||||
|
||||
await registry.start();
|
||||
console.log(registry);
|
||||
})();
|
||||
|
||||
@@ -1,13 +1,53 @@
|
||||
import HttpServer from './services/web.ts';
|
||||
import IHttpServer, {
|
||||
HttpServerOptions,
|
||||
StatusCode,
|
||||
} from './services/web/index.ts';
|
||||
import IDatabase, { DatabaseOptions } from './services/db/index.ts';
|
||||
|
||||
const HttpServer = require('./services/web/worker.ts');
|
||||
const Database = require('./services/db/worker.ts');
|
||||
|
||||
export default class GroupRegistry {
|
||||
#http: HttpServer;
|
||||
constructor() {
|
||||
this.#http = new HttpServer();
|
||||
}
|
||||
#options?: HttpServerOptions & DatabaseOptions;
|
||||
#http?: IHttpServer;
|
||||
#db?: IDatabase;
|
||||
|
||||
constructor(options?: HttpServerOptions & DatabaseOptions) {
|
||||
this.#options = options;
|
||||
}
|
||||
async start() {
|
||||
await this.#http.start();
|
||||
console.log('started');
|
||||
if (Database) {
|
||||
this.#db = new Database(this.#options) as IDatabase;
|
||||
if (!Database.injected && this.#db.start) {
|
||||
await this.#db.start();
|
||||
}
|
||||
}
|
||||
|
||||
if (HttpServer) {
|
||||
this.#http = new IHttpServer(this.#options) as IHttpServer;
|
||||
if (!HttpServer.injected && this.#http.start) {
|
||||
await this.#http.start();
|
||||
}
|
||||
}
|
||||
|
||||
this.#http!.on('/health', (_, res) => {
|
||||
res.writeHead(StatusCode.Ok, { 'Content-Type': 'text' });
|
||||
res.end('ok');
|
||||
});
|
||||
|
||||
this.#http!.on('/groups/:id', async (req, res) => {
|
||||
const { id } = req.params!;
|
||||
const result = await this.#db?.getGroupInfo(id);
|
||||
res.writeHead(StatusCode.Ok, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(result));
|
||||
});
|
||||
|
||||
this.#http!.on('/groups', async (req, res) => {
|
||||
console.time('groups');
|
||||
const result = await this.#db?.getGroups();
|
||||
res.writeHead(StatusCode.Ok, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(result));
|
||||
console.timeEnd('groups');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
112
packages/group/src/services/db/index.ts
Normal file
112
packages/group/src/services/db/index.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { Pool } from 'pg';
|
||||
import groups from '../../../static/groups.json';
|
||||
|
||||
export type DatabaseOptions = {
|
||||
host?: string;
|
||||
pgPort?: number;
|
||||
database?: string;
|
||||
user?: string;
|
||||
password?: string;
|
||||
max?: number;
|
||||
idleTimeoutMillis?: number;
|
||||
};
|
||||
|
||||
export type GroupInfo = {
|
||||
groupId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
iconUrl: string;
|
||||
};
|
||||
|
||||
export default class Database {
|
||||
#pool: Pool;
|
||||
|
||||
constructor(options?: DatabaseOptions) {
|
||||
this.#pool = new Pool(options);
|
||||
}
|
||||
|
||||
async start() {
|
||||
await this.#pool.connect();
|
||||
await this.#prepareTables();
|
||||
}
|
||||
|
||||
async #prepareTables() {
|
||||
await this.#pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
commitment VARCHAR(66) PRIMARY KEY,
|
||||
group_id VARCHAR(255) NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS users_group_idx ON users (group_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS groups (
|
||||
group_id VARCHAR(255) PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description VARCHAR(4095) NOT NULL,
|
||||
icon_url VARCHAR(255)
|
||||
);
|
||||
`);
|
||||
|
||||
for (const group of groups) {
|
||||
await this.#pool.query(
|
||||
`
|
||||
INSERT INTO groups(group_id, title, description)
|
||||
SELECT $1::text, $2, $3
|
||||
WHERE
|
||||
NOT EXISTS (
|
||||
SELECT * FROM groups WHERE group_id = $1::text
|
||||
);
|
||||
`,
|
||||
[group.id, group.title, group.description],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async insertCommitment(commitment: string, groupId: string) {
|
||||
await this.#pool.query(
|
||||
`
|
||||
INSERT INTO users(commitment, group_id)
|
||||
VALUES ($1, $2)
|
||||
`,
|
||||
[commitment, groupId],
|
||||
);
|
||||
}
|
||||
|
||||
async updateCommitment(
|
||||
newCommitment: string,
|
||||
groupId: string,
|
||||
oldCommitment: string,
|
||||
) {
|
||||
await this.#pool.query(
|
||||
`
|
||||
UPDATE users(commitment, group_id)
|
||||
SET commitment = $1
|
||||
WHERE group_id = $2 AND commitment = $3
|
||||
`,
|
||||
[newCommitment, groupId, oldCommitment],
|
||||
);
|
||||
}
|
||||
|
||||
async getGroupInfo(groupId: string): Promise<GroupInfo | null> {
|
||||
const res = await this.#pool.query(
|
||||
`
|
||||
SELECT * FROM groups
|
||||
WHERE group_id = $1
|
||||
`,
|
||||
[groupId],
|
||||
);
|
||||
|
||||
return res?.rows[0] || null;
|
||||
}
|
||||
|
||||
async getGroups(): Promise<GroupInfo[]> {
|
||||
const res = await this.#pool.query(
|
||||
`
|
||||
SELECT * FROM groups
|
||||
`,
|
||||
[],
|
||||
);
|
||||
|
||||
return res.rows;
|
||||
}
|
||||
}
|
||||
14
packages/group/src/services/db/worker.ts
Normal file
14
packages/group/src/services/db/worker.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Worker, isMainThread } from 'worker_threads';
|
||||
import Database from './index.ts';
|
||||
|
||||
function initializeClass() {
|
||||
new Worker(__filename);
|
||||
}
|
||||
|
||||
initializeClass.injected = true;
|
||||
|
||||
if (isMainThread) {
|
||||
module.exports = initializeClass;
|
||||
} else {
|
||||
module.exports = Database;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import * as http from 'http';
|
||||
import { parse } from 'url';
|
||||
import logger from '../../../utilities/src/logger';
|
||||
|
||||
export type HttpServerOptions = {
|
||||
port?: number;
|
||||
};
|
||||
|
||||
export default class HttpServer {
|
||||
#port: number;
|
||||
|
||||
constructor(options?: HttpServerOptions) {
|
||||
const { port = 11014 } = options || {};
|
||||
this.#port = port;
|
||||
}
|
||||
|
||||
async start() {
|
||||
return new Promise((resolve) => {
|
||||
const server = http.createServer(this.#onServerStart);
|
||||
server.listen(this.#port, () => {
|
||||
logger.info(`Server is running on port ${this.#port}`);
|
||||
resolve(server);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#onServerStart = (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
const url = parse(req.url || '', true);
|
||||
console.log(url);
|
||||
res.end('a');
|
||||
};
|
||||
}
|
||||
70
packages/group/src/services/web/index.ts
Normal file
70
packages/group/src/services/web/index.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { parse } from 'url';
|
||||
import logger from '../../../../utilities/src/logger.ts';
|
||||
import { testPath } from '../../../../utilities/src/encoding.ts';
|
||||
import * as http from 'http';
|
||||
|
||||
export type HttpServerOptions = {
|
||||
port?: number;
|
||||
};
|
||||
|
||||
export enum StatusCode {
|
||||
Ok = 200,
|
||||
Created = 201,
|
||||
BadRequest = 400,
|
||||
Unauthorized = 401,
|
||||
Forbidden = 403,
|
||||
NotFound = 404,
|
||||
InternalServerError = 500,
|
||||
NotImplemented = 501,
|
||||
BadGateway = 502,
|
||||
ServiceUnavailable = 503,
|
||||
}
|
||||
|
||||
export type Request = http.IncomingMessage & {
|
||||
params?: { [key: string]: string };
|
||||
};
|
||||
|
||||
export default class HttpServer {
|
||||
#port: number;
|
||||
#handlers: [string, (req: Request, res: http.ServerResponse) => void][] = [];
|
||||
#server: http.Server;
|
||||
|
||||
constructor(options?: HttpServerOptions) {
|
||||
const { port = 11014 } = options || {};
|
||||
this.#port = port;
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this.#server) return;
|
||||
new Promise((resolve) => {
|
||||
this.#server = http.createServer(this.#onRequest);
|
||||
this.#server.listen(this.#port, () => {
|
||||
logger.info(`Server is running on port ${this.#port}`);
|
||||
resolve(this.#server);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#onRequest = (req: Request, res: http.ServerResponse) => {
|
||||
const url = parse(req.url || '', true);
|
||||
|
||||
for (const [pattern, handler] of this.#handlers) {
|
||||
const params = testPath(pattern, url.pathname);
|
||||
if (params) {
|
||||
req.params = params;
|
||||
handler(req, res);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.writeHead(StatusCode.NotImplemented, { 'Content-Type': 'text' });
|
||||
res.end('not implememted');
|
||||
};
|
||||
|
||||
on = (
|
||||
path: string,
|
||||
handler: (req: Request, res: http.ServerResponse) => void,
|
||||
) => {
|
||||
this.#handlers.push([path, handler]);
|
||||
};
|
||||
}
|
||||
14
packages/group/src/services/web/worker.ts
Normal file
14
packages/group/src/services/web/worker.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Worker, isMainThread } from 'worker_threads';
|
||||
import HttpServer from './index.ts';
|
||||
|
||||
function initializeClass() {
|
||||
const worker = new Worker(__filename);
|
||||
}
|
||||
|
||||
initializeClass.injected = true;
|
||||
|
||||
if (isMainThread) {
|
||||
module.exports = initializeClass;
|
||||
} else {
|
||||
module.exports = HttpServer;
|
||||
}
|
||||
82
packages/group/static/groups.json
Normal file
82
packages/group/static/groups.json
Normal file
@@ -0,0 +1,82 @@
|
||||
[
|
||||
{
|
||||
"id": "twitter_common",
|
||||
"title": "Common Twitter User",
|
||||
"description": "Any Twitter users"
|
||||
},
|
||||
{
|
||||
"id": "twitter_uncommon",
|
||||
"title": "Uncommon Twitter User",
|
||||
"description": "Any Twitter users with 100+ followers"
|
||||
},
|
||||
{
|
||||
"id": "twitter_rare",
|
||||
"title": "Rare Twitter User",
|
||||
"description": "Any Twitter users with 1k+ followers"
|
||||
},
|
||||
{
|
||||
"id": "twitter_epic",
|
||||
"title": "Epic Twitter User",
|
||||
"description": "Any Twitter users with 10k+ followers"
|
||||
},
|
||||
{
|
||||
"id": "twitter_legendary",
|
||||
"title": "Legendary Twitter User",
|
||||
"description": "Any Twitter users with 100k+ followers"
|
||||
},
|
||||
{
|
||||
"id": "twitter_mythic",
|
||||
"title": "Mythic Twitter User",
|
||||
"description": "Any Twitter users with 1M+ followers"
|
||||
},
|
||||
{
|
||||
"id": "reddit_common",
|
||||
"title": "Common Reddit User",
|
||||
"description": "Any Reddit users"
|
||||
},
|
||||
{
|
||||
"id": "reddit_uncommon",
|
||||
"title": "Uncommon Reddit User",
|
||||
"description": "Any Reddit users with 2k+ karma"
|
||||
},
|
||||
{
|
||||
"id": "reddit_rare",
|
||||
"title": "Rare Reddit User",
|
||||
"description": "Any Reddit users with 20k+ karma"
|
||||
},
|
||||
{
|
||||
"id": "reddit_epic",
|
||||
"title": "Epic Reddit User",
|
||||
"description": "Any Reddit users with 200k+ karma"
|
||||
},
|
||||
{
|
||||
"id": "reddit_legendary",
|
||||
"title": "Legendary Reddit User",
|
||||
"description": "Any Reddit users with 500k+ karma"
|
||||
},
|
||||
{
|
||||
"id": "github_common",
|
||||
"title": "Common Github User",
|
||||
"description": "Any Github users"
|
||||
},
|
||||
{
|
||||
"id": "github_uncommon",
|
||||
"title": "Uncommon Github User",
|
||||
"description": "Any Github users with under 10+ stars"
|
||||
},
|
||||
{
|
||||
"id": "github_rare",
|
||||
"title": "Rare Github User",
|
||||
"description": "Any Github users with 100+ stars"
|
||||
},
|
||||
{
|
||||
"id": "github_epic",
|
||||
"title": "Epic Github User",
|
||||
"description": "Any Github users with 1000+ stars"
|
||||
},
|
||||
{
|
||||
"id": "github_legendary",
|
||||
"title": "Legendary Github User",
|
||||
"description": "Any Github users with 10000+ stars"
|
||||
}
|
||||
]
|
||||
@@ -277,7 +277,7 @@ tape('Message Format', (t) => {
|
||||
subtype: GroupSubtype.MemberRequest,
|
||||
creator: '0xa1b77ccf93a2b14174c322d673a87bfa0031a2d2',
|
||||
createdAt: new Date(0x018a01173656),
|
||||
groupId: 'group-id',
|
||||
groupId: 'db-id',
|
||||
data: ['a', 'ab', 'abcdefg'],
|
||||
});
|
||||
|
||||
@@ -288,7 +288,7 @@ tape('Message Format', (t) => {
|
||||
subtype: GroupSubtype.Broadcast,
|
||||
creator: '0xa1b77ccf93a2b14174c322d673a87bfa0031a2d2',
|
||||
createdAt: new Date(0x018a01173656),
|
||||
groupId: 'group-id',
|
||||
groupId: 'db-id',
|
||||
data: msgA.hex,
|
||||
});
|
||||
|
||||
@@ -318,7 +318,7 @@ tape('Message Format', (t) => {
|
||||
subtype: GroupSubtype.MemberRequest,
|
||||
creator: '0xa1b77ccf93a2b14174c322d673a87bfa0031a2d2',
|
||||
createdAt: new Date(0x018a01173656),
|
||||
groupId: 'group-id',
|
||||
groupId: 'db-id',
|
||||
data: ['a', 'ab', 'abcdefg'],
|
||||
proof: undefined,
|
||||
},
|
||||
@@ -333,7 +333,7 @@ tape('Message Format', (t) => {
|
||||
subtype: GroupSubtype.Broadcast,
|
||||
creator: '0xa1b77ccf93a2b14174c322d673a87bfa0031a2d2',
|
||||
createdAt: new Date(0x018a01173656),
|
||||
groupId: 'group-id',
|
||||
groupId: 'db-id',
|
||||
data: msgA.hex,
|
||||
proof: undefined,
|
||||
},
|
||||
|
||||
26
packages/utilities/src/encoding.ts
Normal file
26
packages/utilities/src/encoding.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export function testPath(pattern: string, path?: string | null) {
|
||||
const tmpls = pattern.split("/");
|
||||
const parts = (path || "").split("/");
|
||||
const params: { [key: string]: string } = {};
|
||||
let isWildcard = false;
|
||||
|
||||
for (let i = 1; i < tmpls.length; i++) {
|
||||
const tmpl = tmpls[i];
|
||||
const part = parts[i];
|
||||
isWildcard = isWildcard || tmpl === "*";
|
||||
const isParam = tmpl.slice(0, 1) === ":";
|
||||
const isOptional = tmpl.slice(-1) === "?";
|
||||
const paramName = tmpl.replace(/[:?]/g, "");
|
||||
if (isWildcard) {
|
||||
} else if (!isParam) {
|
||||
if (part !== tmpl) return false;
|
||||
} else if (isParam && !isOptional) {
|
||||
if (!part) return;
|
||||
params[paramName] = part;
|
||||
} else if (isParam && isOptional) {
|
||||
params[paramName] = part;
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
Reference in New Issue
Block a user