This commit is contained in:
xion 2024-09-20 22:04:27 +08:00
parent e3991379df
commit a8f80abc88
15 changed files with 892 additions and 66 deletions

View File

@ -21,7 +21,8 @@
"docker:run": "docker run -it --name code-flow -p 4000:4000 docker.xiongxiao.me/code-flow:v0.0.2", "docker:run": "docker run -it --name code-flow -p 4000:4000 docker.xiongxiao.me/code-flow:v0.0.2",
"docker:build:gitea": "docker build -t git.xiongxiao.me/abearxiong/code-flow:v0.0.2 .", "docker:build:gitea": "docker build -t git.xiongxiao.me/abearxiong/code-flow:v0.0.2 .",
"docker:push:gitea": "docker push git.xiongxiao.me/abearxiong/code-flow:v0.0.2", "docker:push:gitea": "docker push git.xiongxiao.me/abearxiong/code-flow:v0.0.2",
"dts": "./node_modules/.bin/dts-bundle-generator -o types/index.d.ts src/type.ts" "dts": "./node_modules/.bin/dts-bundle-generator -o types/index.d.ts src/type.ts",
"postinstall": "patch-package"
}, },
"keywords": [], "keywords": [],
"types": "types/index.d.ts", "types": "types/index.d.ts",
@ -30,18 +31,21 @@
], ],
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@abearxiong/router": "0.0.1-alpha.27", "@abearxiong/router": "0.0.1-alpha.28",
"@abearxiong/use-config": "^0.0.1", "@abearxiong/use-config": "^0.0.2",
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.4", "@babel/preset-env": "^7.25.4",
"@babel/preset-typescript": "^7.24.7", "@babel/preset-typescript": "^7.24.7",
"@types/semver": "^7.5.8",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dts-bundle-generator": "^9.5.1", "dts-bundle-generator": "^9.5.1",
"json5": "^2.2.3", "json5": "^2.2.3",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"minio": "^8.0.1",
"nanoid": "^5.0.7", "nanoid": "^5.0.7",
"pg": "^8.12.0", "pg": "^8.13.0",
"semver": "^7.6.3",
"sequelize": "^6.37.3", "sequelize": "^6.37.3",
"socket.io": "^4.7.5", "socket.io": "^4.7.5",
"strip-ansi": "^7.1.0", "strip-ansi": "^7.1.0",
@ -51,7 +55,7 @@
"devDependencies": { "devDependencies": {
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/jest": "^29.5.13", "@types/jest": "^29.5.13",
"@types/jsonwebtoken": "^9.0.6", "@types/jsonwebtoken": "^9.0.7",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^22.5.5", "@types/node": "^22.5.5",
"@types/superagent": "^8.1.9", "@types/superagent": "^8.1.9",
@ -62,7 +66,9 @@
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"fork-ts-checker-webpack-plugin": "^9.0.2", "fork-ts-checker-webpack-plugin": "^9.0.2",
"jest": "^29.7.0", "jest": "^29.7.0",
"nodemon": "^3.1.4", "nodemon": "^3.1.6",
"patch-package": "^8.0.0",
"postinstall-postinstall": "^2.1.0",
"supertest": "^7.0.0", "supertest": "^7.0.0",
"ts-jest": "^29.2.5", "ts-jest": "^29.2.5",
"ts-loader": "^9.5.1", "ts-loader": "^9.5.1",
@ -71,5 +77,10 @@
"webpack": "^5.94.0", "webpack": "^5.94.0",
"webpack-cli": "^5.1.4", "webpack-cli": "^5.1.4",
"webpack-node-externals": "^3.0.0" "webpack-node-externals": "^3.0.0"
},
"pnpm": {
"patchedDependencies": {
"@abearxiong/router@0.0.1-alpha.28": "patches/@abearxiong__router@0.0.1-alpha.28.patch"
}
} }
} }

View File

@ -0,0 +1,41 @@
diff --git a/dist/route.d.ts b/dist/route.d.ts
index 28ae5103aea403c9bf71f7fc897d3d0aecd17c70..f900e21871df3586f05105504228695e31725256 100644
--- a/dist/route.d.ts
+++ b/dist/route.d.ts
@@ -1,4 +1,5 @@
import { Schema, Rule } from './validator/index.ts';
+export type RouterContextT = { code?:number, [key: string]: any};
export type RouteContext<T = {
code?: number;
}, S = any> = {
@@ -22,7 +23,7 @@ export type RouteContext<T = {
end?: boolean;
queryRouter?: QueryRouter;
} & T;
-export type Run<T = any> = (ctx?: RouteContext<T>) => Promise<typeof ctx | null>;
+export type Run<T = any> = (ctx?: RouteContext<T>) => Promise<RouteContext<T> | null | void>;
export type NextRoute = Pick<Route, 'id' | 'path' | 'key'>;
export type RouteOpts = {
path?: string;
@@ -43,7 +44,7 @@ export type RouteOpts = {
verifyKey?: (key: string, ctx?: RouteContext, dev?: boolean) => boolean;
idUsePath?: boolean;
};
-export type DefineRouteOpts = Omit<RouteOpts, 'idUsePath' | 'verify' | 'verifyKey'>;
+export type DefineRouteOpts = Omit<RouteOpts, 'idUsePath' | 'verify' | 'verifyKey' | 'nextRoute'>;
declare const pickValue: readonly ["path", "key", "id", "description", "type", "validator", "middleware"];
export type RouteInfo = Pick<Route, (typeof pickValue)[number]>;
export declare class Route {
@@ -109,9 +110,9 @@ export declare class Route {
error?: undefined;
};
define(opts: DefineRouteOpts): this;
- define<T>(fn: Run<T>): this;
- define<T>(key: string, fn: Run<T>): this;
- define<T>(path: string, key: string, fn: Run<T>): this;
+ define<T extends {[key:string]:any} = RouterContextT >(fn: Run<T>): this;
+ define<T extends {[key:string]:any} = RouterContextT >(key: string, fn: Run<T>): this;
+ define<T extends {[key:string]:any} = RouterContextT>(path: string, key: string, fn: Run<T>): this;
addTo(router: QueryRouter | {
add: (route: Route) => void;
[key: string]: any;

529
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1 +1,3 @@
export { sequelize } from './sequelize.ts'; export { sequelize } from './sequelize.ts';
export * from './minio.ts'

14
src/modules/minio.ts Normal file
View File

@ -0,0 +1,14 @@
import { Client, ClientOptions } from 'minio';
import { useConfig } from '@abearxiong/use-config';
type MinioConfig = {
minio: ClientOptions & { bucketName: string };
};
const config = useConfig<MinioConfig>();
const { bucketName, ...minioRest } = config.minio;
export const minioClient = new Client(minioRest);
export { bucketName };
if (!minioClient) {
throw new Error('Minio client not initialized');
}

View File

@ -1,13 +1,15 @@
import { CustomError } from '@abearxiong/router'; import { CustomError } from '@abearxiong/router';
import { app } from '../../app.ts'; import { app } from '../../app.ts';
import { ContainerModel, ContainerData, Container } from './models/index.ts'; import { ContainerModel, ContainerData, Container } from './models/index.ts';
import semver from 'semver';
const list = app.route({ const list = app.route({
path: 'container', path: 'container',
key: 'list', key: 'list',
}); });
list.run = async (ctx) => { list.run = async (ctx) => {
const list = await ContainerModel.findAll(); const list = await ContainerModel.findAll({
order: [['updatedAt', 'DESC']],
});
ctx.body = list; ctx.body = list;
return ctx; return ctx;
}; };
@ -91,3 +93,49 @@ deleteRoute.run = async (ctx) => {
return ctx; return ctx;
}; };
deleteRoute.addTo(app); deleteRoute.addTo(app);
const publish = app.route({
path: 'container',
key: 'publish',
});
publish.nextRoute = { path: 'resource', key: 'publishContainer' };
publish
.define(async (ctx) => {
const { data } = ctx.query;
const { id, publish, type = 'patch', beta = false } = data;
type PublishType = 'patch' | 'minor' | 'major';
if (!id) {
throw new CustomError('id is required');
}
const container = await ContainerModel.findByPk(id);
if (!container) {
throw new CustomError('container not found');
}
const { name, description } = publish;
const oldPublish = container.publish;
let _version = '';
if (!oldPublish.version) {
if (publish.name) {
throw new CustomError('publish name is required');
}
container.publish = {
name,
description,
version: '0.0.1',
};
} else {
_version = semver.inc(oldPublish.version, type as PublishType, beta ? 'beta' : undefined);
container.publish.version = _version;
}
if (ctx.state) {
ctx.state.container = container;
} else {
ctx.state = {
container,
};
}
// 执行下一步操作了
ctx.body = 'run ok';
})
.addTo(app);

View File

@ -7,16 +7,14 @@ export interface ContainerData {
showChild?: boolean; showChild?: boolean;
shadowRoot?: boolean; shadowRoot?: boolean;
} }
export type Container = { export type ContainerPublish = {
id?: string; rid?: string; // resource id
title?: string; name?: string;
description?: string; description?: string;
type?: string; version?: string;
code?: string;
source?: string;
sourceType?: string;
data?: ContainerData;
}; };
export type Container = Partial<InstanceType<typeof ContainerModel>>;
export class ContainerModel extends Model { export class ContainerModel extends Model {
declare id: string; declare id: string;
declare title: string; declare title: string;
@ -26,6 +24,12 @@ export class ContainerModel extends Model {
declare source: string; declare source: string;
declare sourceType: string; declare sourceType: string;
declare data: ContainerData; declare data: ContainerData;
declare publish: ContainerPublish;
declare uid: string;
// timestamps
public readonly createdAt!: Date;
public readonly updatedAt!: Date;
} }
ContainerModel.init( ContainerModel.init(
{ {
@ -63,6 +67,11 @@ ContainerModel.init(
type: DataTypes.JSON, type: DataTypes.JSON,
defaultValue: {}, defaultValue: {},
}, },
publish: {
type: DataTypes.JSON,
defaultValue: {},
},
uid: { uid: {
type: DataTypes.UUID, type: DataTypes.UUID,
allowNull: true, allowNull: true,

View File

@ -1,3 +1,5 @@
import './container/index.ts'; import './container/index.ts';
import './page/index.ts'; import './page/index.ts';
import './resource/index.ts';

View File

@ -210,7 +210,7 @@ app
path: 'page', path: 'page',
key: 'getDeck', key: 'getDeck',
}) })
.define(async (ctx) => { .define<any>(async (ctx) => {
const id = ctx.query.id; const id = ctx.query.id;
if (!id) { if (!id) {
throw new CustomError('id is required'); throw new CustomError('id is required');

View File

@ -0,0 +1,2 @@
import './publish.ts';
import './list.ts';

View File

@ -0,0 +1 @@
export * from './publish-minio.ts';

View File

@ -0,0 +1,35 @@
import { Resource } from '../models/index.ts';
import { minioClient, bucketName } from '../../../modules/minio.ts';
type MinioRes = {
etag?: string; // 文件的etag, 用于后续的文件下载
versionId?: string;
};
type PublishOptions = {
name: string;
version: string;
code: string;
};
export const publishJsCode = async ({ name, version, code }: PublishOptions) => {
// publish to minio
const codeBuffer = Buffer.from(code);
const codePath = `${name}/${version}/index.js`;
try {
const res = await minioClient.putObject(bucketName, codePath, codeBuffer, codeBuffer.length, {
'Content-Type': 'application/javascript',
'Cache-Control': 'max-age=31536000, immutable',
});
return {
code: 200,
data: { ...res, path: codePath },
};
} catch (e) {
console.error('publish error', e.message);
return {
code: 500,
message: e.message,
};
}
};

View File

@ -0,0 +1,72 @@
import { ResourceData, ResourceModel } from './models/index.ts';
import { app } from '../../app.ts';
import { CustomError } from '@abearxiong/router';
app
.route({
path: 'resource',
key: 'list',
})
.define(async (ctx) => {
const list = await ResourceModel.findAll({
order: [['updatedAt', 'DESC']],
});
ctx.body = list;
return ctx;
})
.addTo(app);
app
.route({
path: 'resource',
key: 'get',
})
.define(async (ctx) => {
const id = ctx.query.id;
if (!id) {
throw new CustomError('id is required');
}
const rm = await ResourceModel.findByPk(id);
if (!rm) {
throw new CustomError('resource not found');
}
ctx.body = rm;
return ctx;
})
.addTo(app);
app
.route({ path: 'resource', key: 'update' })
.define(async (ctx) => {
const { data, id, ...rest } = ctx.query.data;
if (id) {
const resource = await ResourceModel.findByPk(id);
if (resource) {
const newResource = await resource.update({ data, ...rest });
ctx.body = newResource;
}
} else if (data) {
const resource = await ResourceModel.create({ data, ...rest });
ctx.body = resource;
}
})
.addTo(app);
app
.route({
path: 'resource',
key: 'delete',
})
.define(async (ctx) => {
const id = ctx.query.id;
if (!id) {
throw new CustomError('id is required');
}
const resource = await ResourceModel.findByPk(id);
if (!resource) {
throw new CustomError('resource not found');
}
await resource.destroy();
ctx.body = 'success';
})
.addTo(app);

View File

@ -0,0 +1,87 @@
import { sequelize } from '../../../modules/sequelize.ts';
import { DataTypes, Model } from 'sequelize';
type FileUrlList = {
path: string;
etag: string;
versionId: string;
};
export interface ResourceData {
list: FileUrlList[];
lastestVersion: string;
updatedAt: string;
[key: string]: any;
}
export const defaultData: ResourceData = {
list: [],
lastestVersion: '0.0.0',
updatedAt: '',
};
export type Resource = {
id?: string;
name?: string;
description?: string;
source?: string;
sourceId?: string;
data?: ResourceData;
version?: string;
uid?: string;
};
export class ResourceModel extends Model {
declare id: string;
declare name: string;
declare description: string;
declare source: string;
declare sourceId: string;
declare data: ResourceData;
declare version: string;
declare uid: string;
}
ResourceModel.init(
{
id: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
comment: 'id',
},
name: {
type: DataTypes.STRING, // 第一次创建之后就不能修改了,因为这个是用来做唯一标识的
defaultValue: '',
},
description: {
type: DataTypes.TEXT,
defaultValue: '',
},
source: {
type: DataTypes.STRING,
defaultValue: '',
},
sourceId: {
type: DataTypes.STRING,
defaultValue: '',
},
version: {
type: DataTypes.STRING,
defaultValue: '0.0.0',
},
data: {
type: DataTypes.JSON,
defaultValue: {},
},
uid: {
type: DataTypes.UUID,
allowNull: true,
},
},
{
sequelize,
tableName: 'kv_resource',
},
);
ResourceModel.sync({ alter: true, logging: false }).catch((e) => {
console.error('ResourceModel sync', e);
});

View File

@ -0,0 +1,69 @@
import { defaultData, Resource, ResourceModel } from './models/index.ts';
import { ContainerModel } from './../container/models/index.ts';
import { app } from '../../app.ts';
import { Op } from 'sequelize';
import { publishJsCode } from './lib/publish-minio.ts';
import { CustomError } from '@abearxiong/router';
app
.route({
path: 'resource',
key: 'publishContainer',
idUsePath: true,
})
.define(async (ctx) => {
const container = ctx.state.container as ContainerModel;
const publish = container.publish;
const code = container.code;
let { name, rid, description, version = '0.0.1' } = publish;
const where = [];
if (rid) {
where.push({ id: rid });
}
if (name) {
where.push({ name });
}
let resource = await ResourceModel.findOne({ where: { [Op.or]: where } });
let isCreate = false;
if (!resource) {
isCreate = true;
resource = await ResourceModel.create({
name,
description,
version,
source: 'container',
sourceId: container.id,
data: {
...defaultData,
updatedAt: new Date().toISOString(),
},
});
}
publish.rid = publish.rid || resource.id;
// TODO: check version
const res = await publishJsCode({ name, version, code });
if (res.code === 200) {
await container.update({ publish });
const { etag, versionId, path } = res.data;
resource.version = version;
const newData = {
list: [],
...resource.data,
};
newData.list.push({
etag,
versionId,
path,
});
newData.lastestVersion = version;
newData.updatedAt = new Date().toISOString();
resource.data = newData;
await resource.save();
ctx.body = { resource, container, resourceIsNew: isCreate };
} else {
throw new CustomError(res.message);
}
// await container.update({ publish });
})
.addTo(app);