& R;
14 |
15 | export type ReactPropsWithChildren = Modify<{ children?: React.ReactNode | undefined }, P>;
16 |
17 | export type ReactFC
= React.FC>;
18 |
--------------------------------------------------------------------------------
/packages/utils/src/client.ts:
--------------------------------------------------------------------------------
1 | export * from './collections-graph';
2 | export * from './common';
3 | export * from './date';
4 | export * from './forEach';
5 | export * from './getValuesByPath';
6 | export * from './json-templates';
7 | export * from './log';
8 | export * from './merge';
9 | export * from './number';
10 | export * from './parse-filter';
11 | export * from './registry';
12 | export * from './isPortalInBody';
13 | export * from './uid';
14 | export * from './url';
15 | export * from './parseHTML';
16 | export * from './dom';
17 | export * from './currencyUtils';
18 |
--------------------------------------------------------------------------------
/packages/components/src/select-table/hooks/useFlatOptions.tsx:
--------------------------------------------------------------------------------
1 | const useFlatOptions = (tree: any[]) => {
2 | return getFlatOptions(tree);
3 | };
4 |
5 | const getFlatOptions = (tree: any[]) => {
6 | const flatData = (data?: any[]) => {
7 | let list: any[] = [];
8 | data?.forEach((item) => {
9 | list = [...list, item];
10 | if (item?.children?.length) {
11 | list = [...list, ...flatData(item.children)];
12 | }
13 | });
14 | return list;
15 | };
16 | return flatData(tree);
17 | };
18 |
19 | export { useFlatOptions, getFlatOptions };
20 |
--------------------------------------------------------------------------------
/packages/database/src/value-parsers/json-value-parser.ts:
--------------------------------------------------------------------------------
1 | import { BaseValueParser } from './base-value-parser';
2 |
3 | export class JsonValueParser extends BaseValueParser {
4 | async setValue(value: any) {
5 | if (typeof value === 'string') {
6 | if (value.trim() === '') {
7 | this.value = null;
8 | } else {
9 | try {
10 | this.value = JSON.parse(value);
11 | } catch (error) {
12 | this.errors.push(error.message);
13 | }
14 | }
15 | } else {
16 | this.value = value;
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/database/src/relation-repository/types.ts:
--------------------------------------------------------------------------------
1 | import { Transactionable } from 'sequelize';
2 |
3 | import { TargetKey, Values } from '../repository';
4 |
5 | export type PrimaryKeyWithThroughValues = [TargetKey, Values];
6 |
7 | export interface AssociatedOptions extends Transactionable {
8 | tk?: TargetKey | TargetKey[] | PrimaryKeyWithThroughValues | PrimaryKeyWithThroughValues[];
9 | }
10 |
11 | export type setAssociationOptions =
12 | | TargetKey
13 | | TargetKey[]
14 | | PrimaryKeyWithThroughValues
15 | | PrimaryKeyWithThroughValues[]
16 | | AssociatedOptions;
17 |
--------------------------------------------------------------------------------
/packages/devkit/templates/plugin/package.json.tpl:
--------------------------------------------------------------------------------
1 | {
2 | "name": "{{{packageName}}}",
3 | "displayName": "Display Name for {{{packageName}}}",
4 | "version": "{{{packageVersion}}}",
5 | "description": "Description for {{{packageName}}}",
6 | "main": "dist/server/index.js",
7 | "devDependencies": {
8 | "@tachybase/client": "catalog:",
9 | "@tachybase/test": "catalog:",
10 | "@tego/client": "catalog:",
11 | "@tego/server": "catalog:"
12 | },
13 | "description.zh-CN": "插件 {{{packageName}}} 的描述",
14 | "displayName.zh-CN": "插件 {{{packageName}}} 的显示名称"
15 | }
16 |
--------------------------------------------------------------------------------
/packages/di/src/interfaces/service-options.interface.ts:
--------------------------------------------------------------------------------
1 | import { ServiceMetadata } from './service-metadata.interface';
2 |
3 | /**
4 | * The public ServiceOptions is partial object of ServiceMetadata and either one
5 | * of the following is set: `type`, `factory`, `value` but not more than one.
6 | */
7 | export type ServiceOptions =
8 | | Omit>, 'referencedBy' | 'type' | 'factory'>
9 | | Omit>, 'referencedBy' | 'value' | 'factory'>
10 | | Omit>, 'referencedBy' | 'value' | 'type'>;
11 |
--------------------------------------------------------------------------------
/packages/utils/src/__tests__/forEach.test.ts:
--------------------------------------------------------------------------------
1 | import { forEach } from '../forEach';
2 |
3 | describe('forEach', () => {
4 | test('array', () => {
5 | const arr = [1, 2, 3];
6 | const result = [];
7 | forEach(arr, (value, index) => {
8 | result.push(value);
9 | });
10 | expect(result).toEqual(arr);
11 | });
12 |
13 | test('object', () => {
14 | const obj = { a: 1, b: 2, c: 3 };
15 | const result = [];
16 | forEach(obj, (value, key) => {
17 | result.push(value);
18 | });
19 | expect(result).toEqual([1, 2, 3]);
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/packages/schema/src/react/components/FormProvider.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { useAttach } from '../hooks/useAttach';
4 | import { ContextCleaner, FormContext } from '../shared';
5 | import { IProviderProps, ReactFC } from '../types';
6 |
7 | export const FormProvider: ReactFC = (props) => {
8 | const form = useAttach(props.form);
9 | return (
10 |
11 | {props.children}
12 |
13 | );
14 | };
15 |
16 | FormProvider.displayName = 'FormProvider';
17 |
--------------------------------------------------------------------------------
/packages/database/src/fields/uuid-field.ts:
--------------------------------------------------------------------------------
1 | import { DataTypes } from 'sequelize';
2 |
3 | import { BaseColumnFieldOptions, Field, FieldContext } from './field';
4 |
5 | export class UuidField extends Field {
6 | constructor(options?: any, context?: FieldContext) {
7 | super(
8 | {
9 | defaultValue: new DataTypes.UUIDV4(),
10 | ...options,
11 | },
12 | context,
13 | );
14 | }
15 | get dataType() {
16 | return DataTypes.UUID;
17 | }
18 | }
19 |
20 | export interface UUIDFieldOptions extends BaseColumnFieldOptions {
21 | type: 'uuid';
22 | }
23 |
--------------------------------------------------------------------------------
/packages/devkit/src/builder/build/index.ts:
--------------------------------------------------------------------------------
1 | import { Options as TsupConfig } from 'tsup';
2 | import { InlineConfig as ViteConfig } from 'vite';
3 |
4 | export * from './utils';
5 |
6 | export type PkgLog = (msg: string, ...args: any[]) => void;
7 |
8 | interface UserConfig {
9 | modifyTsupConfig?: (config: TsupConfig) => TsupConfig;
10 | modifyViteConfig?: (config: ViteConfig) => ViteConfig;
11 | beforeBuild?: (log: PkgLog) => void | Promise;
12 | afterBuild?: (log: PkgLog) => void | Promise;
13 | }
14 |
15 | declare const defineConfig: (config: UserConfig) => UserConfig;
16 |
--------------------------------------------------------------------------------
/packages/core/src/commands/console.ts:
--------------------------------------------------------------------------------
1 | import REPL from 'node:repl';
2 |
3 | import Application from '../application';
4 |
5 | export default (app: Application) => {
6 | app
7 | .command('console')
8 | .preload()
9 | .action(async () => {
10 | await app.start();
11 | const repl = (REPL.start('tachybase > ').context.app = app);
12 | repl.on('exit', async function (err) {
13 | if (err) {
14 | console.error(err);
15 | process.exit(1);
16 | }
17 | await app.stop();
18 | process.exit(0);
19 | });
20 | });
21 | };
22 |
--------------------------------------------------------------------------------
/packages/database/src/relation-repository/belongs-to-repository.ts:
--------------------------------------------------------------------------------
1 | import { BelongsTo } from 'sequelize';
2 |
3 | import { SingleRelationFindOption, SingleRelationRepository } from './single-relation-repository';
4 |
5 | type BelongsToFindOptions = SingleRelationFindOption;
6 |
7 | export class BelongsToRepository extends SingleRelationRepository {
8 | async filterOptions(sourceModel) {
9 | const association = this.association as BelongsTo;
10 |
11 | return {
12 | // @ts-ignore
13 | [association.targetKey]: sourceModel.get(association.foreignKey),
14 | };
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/schema/src/reactive/observable.ts:
--------------------------------------------------------------------------------
1 | import * as annotations from './annotations';
2 | import { MakeObModelSymbol } from './environment';
3 | import { createObservable } from './internals';
4 |
5 | export function observable(target: T): T {
6 | return createObservable(null, null, target);
7 | }
8 |
9 | observable.box = annotations.box;
10 | observable.ref = annotations.ref;
11 | observable.deep = annotations.observable;
12 | observable.shallow = annotations.shallow;
13 | observable.computed = annotations.computed;
14 | observable[MakeObModelSymbol] = annotations.observable;
15 |
--------------------------------------------------------------------------------
/packages/database/src/query-interface/query-interface-builder.ts:
--------------------------------------------------------------------------------
1 | import Database from '../database';
2 | import MysqlQueryInterface from './mysql-query-interface';
3 | import PostgresQueryInterface from './postgres-query-interface';
4 | import SqliteQueryInterface from './sqlite-query-interface';
5 |
6 | export default function buildQueryInterface(db: Database) {
7 | const map = {
8 | mysql: MysqlQueryInterface,
9 | mariadb: MysqlQueryInterface,
10 | postgres: PostgresQueryInterface,
11 | sqlite: SqliteQueryInterface,
12 | };
13 |
14 | return new map[db.options.dialect](db);
15 | }
16 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - packages/**
3 |
4 | allowedDeprecatedVersions:
5 | cache-manager-redis-yet: 4.2.0
6 |
7 | neverBuiltDependencies:
8 | - canvas
9 |
10 | nodeLinker: isolated
11 |
12 | overrides:
13 | '@ant-design/cssinjs': 1.24.0
14 | '@types/node': 20.17.10
15 | '@types/react': 18.3.23
16 | '@types/react-dom': 18.3.7
17 | antd: 5.22.5
18 | dayjs: 1.11.13
19 | react: 18.3.1
20 | react-dom: 18.3.1
21 | react-router: 6.28.1
22 | react-router-dom: 6.28.1
23 | string-width: 4.2.3
24 | strip-ansi: 6.0.1
25 | wrap-ansi: 7.0.0
26 | prebuild-install: 7.1.3
27 |
--------------------------------------------------------------------------------
/packages/core/src/middlewares/data-template.ts:
--------------------------------------------------------------------------------
1 | import { Context } from '@tachybase/actions';
2 | import { traverseJSON } from '@tachybase/database';
3 |
4 | export const dataTemplate = async (ctx: Context, next) => {
5 | const { resourceName, actionName } = ctx.action;
6 | const { isTemplate, fields } = ctx.action.params;
7 |
8 | await next();
9 |
10 | if (isTemplate && actionName === 'get' && fields.length > 0) {
11 | ctx.body = traverseJSON(JSON.parse(JSON.stringify(ctx.body)), {
12 | collection: ctx.db.getCollection(resourceName),
13 | include: fields,
14 | });
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/packages/database/src/decorators/target-collection-decorator.ts:
--------------------------------------------------------------------------------
1 | import lodash from 'lodash';
2 |
3 | const injectTargetCollection = (originalMethod: any) => {
4 | const oldValue = originalMethod;
5 |
6 | const newMethod = function (...args: any[]) {
7 | const options = args[0];
8 | const values = options.values;
9 |
10 | if (lodash.isPlainObject(values) && values.__collection) {
11 | options.targetCollection = values.__collection;
12 | }
13 |
14 | return oldValue.apply(this, args);
15 | };
16 |
17 | return newMethod;
18 | };
19 |
20 | export default injectTargetCollection;
21 |
--------------------------------------------------------------------------------
/packages/core/src/migration.ts:
--------------------------------------------------------------------------------
1 | import { Migration as DbMigration } from '@tachybase/database';
2 |
3 | import Application from './application';
4 | import Plugin from './plugin';
5 | import { PluginManager } from './plugin-manager';
6 |
7 | export class Migration extends DbMigration {
8 | appVersion = '';
9 | pluginVersion = '';
10 | on = 'afterLoad';
11 |
12 | get app() {
13 | return this.context.app as Application;
14 | }
15 |
16 | get pm() {
17 | return this.context.app.pm as PluginManager;
18 | }
19 |
20 | get plugin() {
21 | return this.context.plugin as Plugin;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/packages/schema/src/path/contexts.ts:
--------------------------------------------------------------------------------
1 | export type Context = {
2 | flag: string;
3 | [key: string]: any;
4 | };
5 |
6 | const ContextType = (flag: string, props?: any): Context => {
7 | return {
8 | flag,
9 | ...props,
10 | };
11 | };
12 |
13 | export const bracketContext = ContextType('[]');
14 |
15 | export const bracketArrayContext = ContextType('[\\d]');
16 |
17 | export const bracketDContext = ContextType('[[]]');
18 |
19 | export const parenContext = ContextType('()');
20 |
21 | export const braceContext = ContextType('{}');
22 |
23 | export const destructorContext = ContextType('{x}');
24 |
--------------------------------------------------------------------------------
/packages/schema/src/react/components/ExpressionScope.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 |
3 | import { lazyMerge } from '../../shared';
4 | import { SchemaExpressionScopeContext } from '../shared';
5 | import { IExpressionScopeProps, ReactFC } from '../types';
6 |
7 | export const ExpressionScope: ReactFC = (props) => {
8 | const scope = useContext(SchemaExpressionScopeContext);
9 | return (
10 |
11 | {props.children}
12 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/packages/acl/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tachybase/acl",
3 | "version": "1.6.1",
4 | "description": "",
5 | "license": "Apache-2.0",
6 | "main": "./lib/index.js",
7 | "types": "./lib/index.d.ts",
8 | "dependencies": {
9 | "@tachybase/actions": "workspace:*",
10 | "@tachybase/resourcer": "workspace:*",
11 | "@tachybase/utils": "workspace:*",
12 | "koa-compose": "^4.1.0",
13 | "lodash": "4.17.21",
14 | "minimatch": "^5.1.6"
15 | },
16 | "devDependencies": {
17 | "@types/lodash": "4.17.20",
18 | "@types/minimatch": "^6.0.0",
19 | "@types/node": "20.17.10"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/packages/devkit/src/builder/buildable-packages/skip-package.ts:
--------------------------------------------------------------------------------
1 | import { getPkgLog } from '../build/utils';
2 | import { IBuildablePackage, IBuildContext } from '../interfaces';
3 |
4 | export class SkipPackage implements IBuildablePackage {
5 | static name = 'skip';
6 | name: string;
7 | dir: string;
8 | context: IBuildContext;
9 |
10 | constructor(name: string, dir: string, context: IBuildContext) {
11 | this.name = name;
12 | this.dir = dir;
13 | this.context = context;
14 | }
15 | async build() {
16 | const log = getPkgLog(this.name);
17 | log('skip ', this.name);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/resourcer/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tachybase/resourcer",
3 | "version": "1.6.1",
4 | "description": "",
5 | "license": "Apache-2.0",
6 | "main": "./lib/index.js",
7 | "types": "./lib/index.d.ts",
8 | "dependencies": {
9 | "@tachybase/utils": "workspace:*",
10 | "glob": "11.0.0",
11 | "koa-compose": "^4.1.0",
12 | "lodash": "^4.17.21",
13 | "path-to-regexp": "^8.2.0",
14 | "qs": "^6.14.0"
15 | },
16 | "devDependencies": {
17 | "@types/node": "20.17.10",
18 | "koa": "^3.0.1",
19 | "koa-bodyparser": "^4.4.1",
20 | "supertest": "^7.1.4"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/packages/actions/src/actions/proxy-to-repository.ts:
--------------------------------------------------------------------------------
1 | import lodash from 'lodash';
2 |
3 | import { Context } from '../index';
4 | import { getRepositoryFromParams } from '../utils';
5 |
6 | export function proxyToRepository(paramKeys: string[], repositoryMethod: string) {
7 | return async function (ctx: Context, next) {
8 | const repository = getRepositoryFromParams(ctx);
9 |
10 | const callObj = lodash.pick(ctx.action.params, paramKeys);
11 | callObj.context = ctx;
12 |
13 | ctx.body = await repository[repositoryMethod](callObj);
14 |
15 | ctx.status = 200;
16 | await next();
17 | };
18 | }
19 |
--------------------------------------------------------------------------------
/packages/components/src/__builtins__/hooks/usePrefixCls.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 |
3 | import { ConfigProvider } from 'antd';
4 |
5 | export const usePrefixCls = (
6 | tag?: string,
7 | props?: {
8 | prefixCls?: string;
9 | },
10 | ) => {
11 | if ('ConfigContext' in ConfigProvider) {
12 | // eslint-disable-next-line react-hooks/rules-of-hooks
13 | const { getPrefixCls } = useContext(ConfigProvider.ConfigContext);
14 | return getPrefixCls(tag, props?.prefixCls);
15 | } else {
16 | const prefix = props?.prefixCls ?? 'ant-';
17 | return `${prefix}${tag ?? ''}`;
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/packages/data-source/src/utils.ts:
--------------------------------------------------------------------------------
1 | export function parseCollectionName(collection: string) {
2 | if (!collection) {
3 | return [];
4 | }
5 | const dataSourceCollection = collection.split(':');
6 | const collectionName = dataSourceCollection.pop();
7 | const dataSourceName = dataSourceCollection[0] ?? 'main';
8 | return [dataSourceName, collectionName];
9 | }
10 |
11 | export function joinCollectionName(dataSourceName: string, collectionName: string) {
12 | if (!dataSourceName || dataSourceName === 'main') {
13 | return collectionName;
14 | }
15 | return `${dataSourceName}:${collectionName}`;
16 | }
17 |
--------------------------------------------------------------------------------
/packages/components/src/form-button-group/style.ts:
--------------------------------------------------------------------------------
1 | import { genStyleHook } from './../__builtins__';
2 |
3 | export default genStyleHook('form-button-group', (token) => {
4 | const { componentCls, antCls, colorBorder } = token;
5 | return {
6 | [componentCls]: {
7 | '&-sticky': {
8 | padding: '10px 0',
9 | borderTop: `1px solid ${colorBorder}`,
10 | zIndex: 999,
11 |
12 | [`${componentCls}-sticky-inner`]: {
13 | display: 'flex',
14 | [`${antCls}-formily-item`]: {
15 | flex: 2,
16 | },
17 | },
18 | },
19 | },
20 | };
21 | });
22 |
--------------------------------------------------------------------------------
/packages/devkit/src/commands/global.ts:
--------------------------------------------------------------------------------
1 | import { Command } from 'commander';
2 |
3 | import { promptForTs, run } from '../util';
4 |
5 | export default (cli: Command) => {
6 | const { APP_SERVER_ROOT, SERVER_TSCONFIG_PATH } = process.env;
7 | cli
8 | .allowUnknownOption()
9 | .option('-h, --help')
10 | .action((options) => {
11 | promptForTs();
12 | run('tsx', [
13 | '--tsconfig',
14 | SERVER_TSCONFIG_PATH ?? '',
15 | '-r',
16 | 'tsconfig-paths/register',
17 | `${APP_SERVER_ROOT}/src/index.ts`,
18 | ...process.argv.slice(2),
19 | ]);
20 | });
21 | };
22 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Reporting security issues
2 |
3 | If you believe you have found a security issue in the Tego, please contact us immediately.
4 |
5 | When reporting a suspected security problem, please bear this in mind:
6 |
7 | * Make sure to provide as many details as possible about the vulnerability.
8 | * Please do not disclose publicly any security issues until we fix them and publish security releases.
9 |
10 | Contact us at https://github.com/tegojs/tego/issues. As soon as we receive the security report, we'll work promptly to confirm the issue and then to provide a security fix. You will receive a response from us within 7 working days.
11 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yaml:
--------------------------------------------------------------------------------
1 | name: Tests
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - main
9 |
10 | jobs:
11 | tests:
12 | runs-on: ubuntu-22.04
13 | steps:
14 | - name: Checkout code
15 | uses: actions/checkout@v3
16 | - name: Setup pnpm
17 | uses: pnpm/action-setup@v4
18 | - name: Setup Node.js
19 | uses: actions/setup-node@v4
20 | with:
21 | node-version: 20
22 | cache: 'pnpm'
23 | - name: Install
24 | run: pnpm install
25 | - name: Tests
26 | run: pnpm vitest run --silent --reporter=dot
27 |
--------------------------------------------------------------------------------
/packages/core/src/commands/install.ts:
--------------------------------------------------------------------------------
1 | import Application from '../application';
2 |
3 | export default (app: Application) => {
4 | app
5 | .command('install')
6 | .ipc()
7 | .auth()
8 | .option('-f, --force')
9 | .option('-c, --clean')
10 | .option('--lang ')
11 | .action(async (options) => {
12 | if (options.lang) {
13 | process.env.INIT_APP_LANG = options.lang;
14 | }
15 | await app.install(options);
16 | const reinstall = options.clean || options.force;
17 | app.logger.info(`app ${reinstall ? 'reinstalled' : 'installed'} successfully [v${app.getVersion()}]`);
18 | });
19 | };
20 |
--------------------------------------------------------------------------------
/packages/core/src/migrations/20240705000001-remove-pkgs-approval.ts:
--------------------------------------------------------------------------------
1 | import { DataTypes } from '@tachybase/database';
2 |
3 | import { Migration } from '../migration';
4 |
5 | export default class extends Migration {
6 | on = 'beforeLoad';
7 | appVersion = '<0.21.65';
8 |
9 | async up() {
10 | console.log('remove workflow notice');
11 | await this.pm.repository.destroy({
12 | filter: {
13 | name: 'workflow-notice',
14 | },
15 | });
16 |
17 | console.log('remove approval');
18 | await this.pm.repository.destroy({
19 | filter: {
20 | name: 'approval',
21 | },
22 | });
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/database/src/operators/index.ts:
--------------------------------------------------------------------------------
1 | import array from './array';
2 | import association from './association';
3 | import boolean from './boolean';
4 | import childCollection from './child-collection';
5 | import date from './date';
6 | import empty from './empty';
7 | import eq from './eq';
8 | import jsonb from './jsonb';
9 | import ne from './ne';
10 | import notIn from './notIn';
11 | import string from './string';
12 |
13 | export default {
14 | ...association,
15 | ...date,
16 | ...array,
17 | ...empty,
18 | ...string,
19 | ...eq,
20 | ...ne,
21 | ...notIn,
22 | ...boolean,
23 | ...childCollection,
24 | ...jsonb,
25 | };
26 |
--------------------------------------------------------------------------------
/packages/database/src/view-collection.ts:
--------------------------------------------------------------------------------
1 | import { Collection, CollectionContext, CollectionOptions } from './collection';
2 |
3 | export class ViewCollection extends Collection {
4 | constructor(options: CollectionOptions, context: CollectionContext) {
5 | options.autoGenId = false;
6 | options.timestamps = false;
7 |
8 | super(options, context);
9 | }
10 |
11 | isView() {
12 | return true;
13 | }
14 |
15 | protected sequelizeModelOptions(): any {
16 | const modelOptions = super.sequelizeModelOptions();
17 | modelOptions.tableName = this.options.viewName || this.options.name;
18 | return modelOptions;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/core/src/main-data-source.ts:
--------------------------------------------------------------------------------
1 | import { DataSourceOptions, SequelizeDataSource } from '@tachybase/data-source';
2 |
3 | export class MainDataSource extends SequelizeDataSource {
4 | init(options: DataSourceOptions = {}) {
5 | const { acl, resourceManager, database } = options;
6 |
7 | this.acl = acl;
8 | this.resourceManager = resourceManager;
9 |
10 | this.collectionManager = this.createCollectionManager({
11 | collectionManager: {
12 | database,
13 | collectionsFilter: (collection) => {
14 | return collection.options.loadedFromCollectionManager;
15 | },
16 | },
17 | });
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/schema/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tachybase/schema",
3 | "version": "1.6.1",
4 | "license": "Apache-2.0",
5 | "main": "lib/index.js",
6 | "types": "lib/index.d.ts",
7 | "dependencies": {
8 | "camel-case": "^4.1.1",
9 | "hoist-non-react-statics": "^3.3.2",
10 | "lower-case": "^2.0.1",
11 | "param-case": "^3.0.4",
12 | "pascal-case": "^3.1.1",
13 | "upper-case": "^2.0.1"
14 | },
15 | "devDependencies": {
16 | "@testing-library/react": "16.3.0",
17 | "immutable": "5.1.3",
18 | "moment": "2.30.1",
19 | "vitest": "3.2.4"
20 | },
21 | "peerDependencies": {
22 | "react": ">=16.8.0"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/schema/src/reactive/action.ts:
--------------------------------------------------------------------------------
1 | import { createBoundaryAnnotation } from './internals';
2 | import { batchEnd, batchScopeEnd, batchScopeStart, batchStart, untrackEnd, untrackStart } from './reaction';
3 | import { IAction } from './types';
4 |
5 | export const action: IAction = createBoundaryAnnotation(
6 | () => {
7 | batchStart();
8 | untrackStart();
9 | },
10 | () => {
11 | untrackEnd();
12 | batchEnd();
13 | },
14 | );
15 |
16 | action.scope = createBoundaryAnnotation(
17 | () => {
18 | batchScopeStart();
19 | untrackStart();
20 | },
21 | () => {
22 | untrackEnd();
23 | batchScopeEnd();
24 | },
25 | );
26 |
--------------------------------------------------------------------------------
/packages/devkit/src/commands/create-plugin.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from 'node:path';
2 |
3 | import { Command } from 'commander';
4 |
5 | import { PluginGenerator } from '../plugin-generator';
6 |
7 | export default (cli: Command) => {
8 | cli
9 | .command('create-plugin')
10 | .argument('')
11 | .allowUnknownOption()
12 | .action(async (name, options) => {
13 | const generator = new PluginGenerator({
14 | baseDir: process.cwd(),
15 | cwd: resolve(process.cwd(), name),
16 | args: options,
17 | context: {
18 | name,
19 | },
20 | });
21 | await generator.run();
22 | });
23 | };
24 |
--------------------------------------------------------------------------------
/packages/components/src/radio/index.tsx:
--------------------------------------------------------------------------------
1 | import { connect, mapProps, mapReadPretty } from '@tachybase/schema';
2 |
3 | import { Radio as AntdRadio } from 'antd';
4 |
5 | import { PreviewText } from '../preview-text';
6 |
7 | export const InternalRadio = connect(
8 | AntdRadio,
9 | mapProps({
10 | value: 'checked',
11 | onInput: 'onChange',
12 | }),
13 | );
14 |
15 | const Group = connect(
16 | AntdRadio.Group,
17 | mapProps({
18 | dataSource: 'options',
19 | }),
20 | mapReadPretty(PreviewText.Select),
21 | );
22 |
23 | export const Radio = Object.assign(InternalRadio, {
24 | __ANT_RADIO: true,
25 | Group,
26 | });
27 |
28 | export default Radio;
29 |
--------------------------------------------------------------------------------
/packages/core/src/commands/db-sync.ts:
--------------------------------------------------------------------------------
1 | import Application from '../application';
2 |
3 | export default (app: Application) => {
4 | app
5 | .command('db:sync')
6 | .auth()
7 | .preload()
8 | .action(async (...cliArgs) => {
9 | const [opts] = cliArgs;
10 | console.log('db sync...');
11 |
12 | const Collection = app.db.getCollection('collections');
13 | if (Collection) {
14 | // @ts-ignore
15 | await Collection.repository.load();
16 | }
17 |
18 | const force = false;
19 | await app.db.sync({
20 | force,
21 | alter: {
22 | drop: force,
23 | },
24 | });
25 | });
26 | };
27 |
--------------------------------------------------------------------------------
/packages/schema/src/reactive/batch.ts:
--------------------------------------------------------------------------------
1 | import { isFn } from './checkers';
2 | import { BatchCount, BatchEndpoints } from './environment';
3 | import { createBoundaryAnnotation } from './internals';
4 | import { batchEnd, batchScopeEnd, batchScopeStart, batchStart } from './reaction';
5 | import { IBatch } from './types';
6 |
7 | export const batch: IBatch = createBoundaryAnnotation(batchStart, batchEnd);
8 | batch.scope = createBoundaryAnnotation(batchScopeStart, batchScopeEnd);
9 | batch.endpoint = (callback?: () => void) => {
10 | if (!isFn(callback)) return;
11 | if (BatchCount.value === 0) {
12 | callback();
13 | } else {
14 | BatchEndpoints.add(callback);
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/packages/core/src/acl/index.ts:
--------------------------------------------------------------------------------
1 | import { ACL } from '@tachybase/acl';
2 |
3 | import { availableActions } from './available-action';
4 |
5 | const configureResources = [
6 | 'roles',
7 | 'users',
8 | 'collections',
9 | 'fields',
10 | 'collections.fields',
11 | 'roles.collections',
12 | 'roles.resources',
13 | 'rolesResourcesScopes',
14 | 'availableActions',
15 | ];
16 |
17 | export function createACL() {
18 | const acl = new ACL();
19 |
20 | for (const [actionName, actionParams] of Object.entries(availableActions)) {
21 | acl.setAvailableAction(actionName, actionParams);
22 | }
23 |
24 | acl.registerConfigResources(configureResources);
25 |
26 | return acl;
27 | }
28 |
--------------------------------------------------------------------------------
/packages/database/src/fields/set-field.ts:
--------------------------------------------------------------------------------
1 | import { ArrayField } from './array-field';
2 | import { BaseColumnFieldOptions } from './field';
3 |
4 | export interface SetFieldOptions extends BaseColumnFieldOptions {
5 | type: 'set';
6 | }
7 |
8 | export class SetField extends ArrayField {
9 | beforeSave = (model) => {
10 | const oldValue = model.get(this.options.name);
11 | if (oldValue) {
12 | model.set(this.options.name, [...new Set(oldValue)]);
13 | }
14 | };
15 |
16 | bind() {
17 | super.bind();
18 | this.on('beforeSave', this.beforeSave);
19 | }
20 |
21 | unbind() {
22 | super.unbind();
23 | this.off('beforeSave', this.beforeSave);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/schema/src/shared/middleware.ts:
--------------------------------------------------------------------------------
1 | export interface IMiddleware {
2 | (payload: Payload, next: (payload?: Payload) => Result): Result;
3 | }
4 |
5 | export const applyMiddleware = (payload: any, fns: IMiddleware[] = []) => {
6 | const compose = (payload: any, fns: IMiddleware[]): Promise => {
7 | const prevPayload = payload;
8 | return Promise.resolve(fns[0](payload, (payload) => compose(payload ?? prevPayload, fns.slice(1))));
9 | };
10 | return new Promise((resolve, reject) => {
11 | compose(
12 | payload,
13 | fns.concat((payload) => {
14 | resolve(payload);
15 | }),
16 | ).catch(reject);
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/packages/test/setup/settings.postgres.js:
--------------------------------------------------------------------------------
1 | const defaultSettings = require('../../tego/presets/settings');
2 |
3 | /** @type {import('@tachybase/globals').Settings} */
4 | module.exports = {
5 | ...defaultSettings,
6 |
7 | logger: {
8 | ...defaultSettings.logger,
9 | level: 'error',
10 | },
11 |
12 | database: {
13 | dialect: 'postgres',
14 | host: 'localhost',
15 | port: 5432,
16 | database: 'postgres',
17 | user: 'tachybase',
18 | password: 'tachybase',
19 | underscored: false,
20 | timezone: '+00:00',
21 | ssl: {
22 | // ca: '',
23 | // key: '',
24 | // cert: '',
25 | // rejectUnauthorized: true,
26 | },
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/packages/utils/src/requireModule.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 | import { pathToFileURL } from 'node:url';
3 |
4 | export function requireModule(m: any) {
5 | if (typeof m === 'string') {
6 | m = require(m);
7 | }
8 |
9 | if (typeof m !== 'object') {
10 | return m;
11 | }
12 |
13 | return m.__esModule ? m.default : m;
14 | }
15 |
16 | export default requireModule;
17 |
18 | export async function importModule(m: string) {
19 | if (!process.env.VITEST) {
20 | return requireModule(m);
21 | }
22 |
23 | if (path.isAbsolute(m)) {
24 | m = pathToFileURL(m).href;
25 | }
26 |
27 | const r = (await import(m)).default;
28 | return r.__esModule ? r.default : r;
29 | }
30 |
--------------------------------------------------------------------------------
/packages/acl/src/__tests__/allow-manager.test.ts:
--------------------------------------------------------------------------------
1 | import { ACL } from '..';
2 | import { AllowManager } from '../allow-manager';
3 |
4 | describe('allow manager', () => {
5 | let acl: ACL;
6 |
7 | beforeEach(() => {
8 | acl = new ACL();
9 | });
10 |
11 | it('should allow star resource', async () => {
12 | const allowManager = new AllowManager(acl);
13 |
14 | allowManager.allow('*', 'download', 'public');
15 |
16 | expect(await allowManager.isAllowed('users', 'download', {})).toBeTruthy();
17 | expect(await allowManager.isAllowed('users', 'fake-method', {})).toBeFalsy();
18 | expect(await allowManager.isAllowed('users', 'other-method', {})).toBeFalsy();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/packages/data-source/src/default-actions/move.ts:
--------------------------------------------------------------------------------
1 | import actions, { Context } from '@tachybase/actions';
2 |
3 | import { getRepositoryFromParams } from './utils';
4 |
5 | const databaseMoveAction = actions.move;
6 |
7 | export async function move(ctx: Context, next) {
8 | const repository = getRepositoryFromParams(ctx);
9 |
10 | if (repository.move) {
11 | ctx.body = await repository.move(ctx.action.params);
12 | await next();
13 | return;
14 | }
15 |
16 | if (repository.database) {
17 | ctx.databaseRepository = repository;
18 | return databaseMoveAction(ctx, next);
19 | }
20 |
21 | throw new Error(`Repository can not handle action move for ${ctx.action.resourceName}`);
22 | }
23 |
--------------------------------------------------------------------------------
/packages/actions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tachybase/actions",
3 | "version": "1.6.1",
4 | "description": "",
5 | "license": "Apache-2.0",
6 | "main": "./lib/index.js",
7 | "types": "./lib/index.d.ts",
8 | "dependencies": {
9 | "@tachybase/cache": "workspace:*",
10 | "@tachybase/database": "workspace:*",
11 | "@tachybase/resourcer": "workspace:*",
12 | "@tachybase/utils": "workspace:*",
13 | "koa": "^2.16.2",
14 | "lodash": "4.17.21",
15 | "sequelize": "^6.37.7"
16 | },
17 | "devDependencies": {
18 | "@types/koa": "^2.15.0",
19 | "@types/lodash": "^4.17.20",
20 | "koa-bodyparser": "^4.4.1",
21 | "qs": "^6.14.0",
22 | "supertest": "^7.1.4"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/evaluators/src/utils/formulajs.ts:
--------------------------------------------------------------------------------
1 | import * as functions from '@formulajs/formulajs';
2 |
3 | import { evaluate } from '.';
4 |
5 | const fnNames = Object.keys(functions).filter((key) => key !== 'default');
6 | const fns = fnNames.map((key) => functions[key]);
7 |
8 | export default evaluate.bind(function (expression: string, scope = {}) {
9 | const fn = new Function(...fnNames, ...Object.keys(scope), `return ${expression}`);
10 | const result = fn(...fns, ...Object.values(scope));
11 | if (typeof result === 'number') {
12 | if (Number.isNaN(result) || !Number.isFinite(result)) {
13 | return null;
14 | }
15 | return functions.ROUND(result, 9);
16 | }
17 | return result;
18 | }, {});
19 |
--------------------------------------------------------------------------------
/packages/schema/src/reactive-react/hooks/index.ts:
--------------------------------------------------------------------------------
1 | import { useCompatEffect } from './useCompatEffect';
2 | import { useCompatFactory } from './useCompatFactory';
3 | import { useDidUpdate } from './useDidUpdate';
4 | import { useForceUpdate } from './useForceUpdate';
5 | import { useLayoutEffect } from './useLayoutEffect';
6 | import { useObserver } from './useObserver';
7 |
8 | export const unstable_useForceUpdate = useForceUpdate;
9 | export const unstable_useCompatEffect = useCompatEffect;
10 | export const unstable_useCompatFactory = useCompatFactory;
11 | export const unstable_useDidUpdate = useDidUpdate;
12 | export const unstable_useLayoutEffect = useLayoutEffect;
13 | export const unstable_useObserver = useObserver;
14 |
--------------------------------------------------------------------------------
/packages/schema/src/validator/template.ts:
--------------------------------------------------------------------------------
1 | import { FormPath, isFn, isStr } from '../shared';
2 | import { getValidateMessageTemplateEngine } from './registry';
3 | import { IValidateResult, IValidatorRules } from './types';
4 |
5 | export const render = (result: IValidateResult, rules: IValidatorRules): IValidateResult => {
6 | const { message } = result;
7 | if (isStr(message)) {
8 | const template = getValidateMessageTemplateEngine();
9 | if (isFn(template)) {
10 | result.message = template(message, rules);
11 | }
12 | result.message = result.message.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, $0) => {
13 | return FormPath.getIn(rules, $0);
14 | });
15 | }
16 | return result;
17 | };
18 |
--------------------------------------------------------------------------------
/.github/workflows/full-build.yaml:
--------------------------------------------------------------------------------
1 | name: Full Build
2 | on:
3 | push:
4 | branches:
5 | - main
6 | - dev
7 | pull_request:
8 | branches:
9 | - main
10 | - dev
11 |
12 | permissions:
13 | contents: write
14 |
15 | jobs:
16 | full-build:
17 | runs-on: ubuntu-22.04
18 | steps:
19 | - name: Checkout code
20 | uses: actions/checkout@v3
21 | - name: Setup pnpm
22 | uses: pnpm/action-setup@v4
23 | - name: Setup Node.js
24 | uses: actions/setup-node@v4
25 | with:
26 | node-version: 20
27 | cache: 'pnpm'
28 | - name: Install deps
29 | run: pnpm install
30 | - name: Build
31 | run: pnpm build
32 |
--------------------------------------------------------------------------------
/packages/core/src/plugin-manager/options/collection.ts:
--------------------------------------------------------------------------------
1 | import { defineCollection } from '@tachybase/database';
2 |
3 | export default defineCollection({
4 | name: 'applicationPlugins',
5 | dumpRules: 'required',
6 | repository: 'PluginManagerRepository',
7 | origin: '@tego/core',
8 | fields: [
9 | { type: 'string', name: 'name', unique: true },
10 | { type: 'string', name: 'packageName', unique: true },
11 | { type: 'string', name: 'version' },
12 | { type: 'boolean', name: 'enabled' },
13 | { type: 'boolean', name: 'installed' },
14 | { type: 'boolean', name: 'builtIn' },
15 | { type: 'json', name: 'options' },
16 | { type: 'boolean', name: 'subView', defaultValue: true },
17 | ],
18 | });
19 |
--------------------------------------------------------------------------------
/packages/components/src/checkbox/index.tsx:
--------------------------------------------------------------------------------
1 | import { connect, mapProps, mapReadPretty } from '@tachybase/schema';
2 |
3 | import { Checkbox as AntdCheckbox } from 'antd';
4 |
5 | import { PreviewText } from '../preview-text';
6 |
7 | const InternalCheckbox = connect(
8 | AntdCheckbox,
9 | mapProps({
10 | value: 'checked',
11 | onInput: 'onChange',
12 | }),
13 | );
14 |
15 | const Group = connect(
16 | AntdCheckbox.Group,
17 | mapProps({
18 | dataSource: 'options',
19 | }),
20 | mapReadPretty(PreviewText.Select, {
21 | mode: 'tags',
22 | }),
23 | );
24 |
25 | export const Checkbox = Object.assign(InternalCheckbox, {
26 | __ANT_CHECKBOX: true,
27 | Group,
28 | });
29 |
30 | export default Checkbox;
31 |
--------------------------------------------------------------------------------
/packages/data-source/src/repository.ts:
--------------------------------------------------------------------------------
1 | import * as console from 'node:console';
2 |
3 | import { IModel, IRepository } from './types';
4 |
5 | export class Repository implements IRepository {
6 | async create(options) {
7 | console.log('Repository.create....');
8 | }
9 | async update(options) {}
10 | async find(options?: any): Promise {
11 | return [];
12 | }
13 | async findOne(options?: any): Promise {
14 | return {};
15 | }
16 | async destroy(options) {}
17 |
18 | count(options?: any): Promise {
19 | return Promise.resolve(undefined);
20 | }
21 |
22 | findAndCount(options?: any): Promise<[IModel[], number]> {
23 | return Promise.resolve([[], undefined]);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/database/src/decorators/must-have-filter-decorator.ts:
--------------------------------------------------------------------------------
1 | const mustHaveFilter = () => (originalMethod: any, context: ClassMethodDecoratorContext) => {
2 | const oldValue = originalMethod;
3 |
4 | const newMethod = function (...args: any[]) {
5 | const options = args[0];
6 |
7 | if (Array.isArray(options.values)) {
8 | return oldValue.apply(this, args);
9 | }
10 |
11 | if (!options?.filter && !options?.filterByTk && !options?.forceUpdate) {
12 | throw new Error(`must provide filter or filterByTk for ${String(context.name)} call, or set forceUpdate to true`);
13 | }
14 |
15 | return oldValue.apply(this, args);
16 | };
17 |
18 | return newMethod;
19 | };
20 |
21 | export default mustHaveFilter;
22 |
--------------------------------------------------------------------------------
/packages/schema/src/shared/deprecate.ts:
--------------------------------------------------------------------------------
1 | import { isFn, isStr } from './checkers';
2 |
3 | const caches = {};
4 |
5 | export function deprecate(
6 | method: any,
7 | message?: string,
8 | help?: string,
9 | ) {
10 | if (isFn(method)) {
11 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
12 | return function (p1?: P1, p2?: P2, p3?: P3, p4?: P4, p5?: P5) {
13 | deprecate(message, help);
14 | return method.apply(this, arguments);
15 | };
16 | }
17 | if (isStr(method) && !caches[method]) {
18 | caches[method] = true;
19 | console.warn(new Error(`${method} has been deprecated. Do not continue to use this api.${message || ''}`));
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/packages/test/src/client/index.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { render } from '@testing-library/react';
4 |
5 | export type { queries } from '@testing-library/dom';
6 |
7 | function customRender(ui: React.ReactElement, options = {}) {
8 | return render(ui, {
9 | // wrap provider(s) here if needed
10 | wrapper: ({ children }) => children,
11 | ...options,
12 | });
13 | }
14 |
15 | export * from '@testing-library/react';
16 | export { default as userEvent } from '@testing-library/user-event';
17 | // override render export
18 | export { customRender as render };
19 |
20 | export const sleep = async (timeout = 0) => {
21 | return new Promise((resolve) => {
22 | setTimeout(resolve, timeout);
23 | });
24 | };
25 |
--------------------------------------------------------------------------------
/packages/data-source/src/data-source-factory.ts:
--------------------------------------------------------------------------------
1 | import { DataSource } from './data-source';
2 |
3 | export class DataSourceFactory {
4 | public collectionTypes: Map = new Map();
5 |
6 | register(type: string, dataSourceClass: typeof DataSource) {
7 | this.collectionTypes.set(type, dataSourceClass);
8 | }
9 |
10 | getClass(type: string): typeof DataSource {
11 | return this.collectionTypes.get(type);
12 | }
13 |
14 | create(type: string, options: any = {}): DataSource {
15 | const klass = this.collectionTypes.get(type);
16 | if (!klass) {
17 | throw new Error(`Data source type "${type}" not found`);
18 | }
19 |
20 | // @ts-ignore
21 | return new klass(options);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/packages/schema/src/reactive-react/hooks/useObserver.ts:
--------------------------------------------------------------------------------
1 | import { Tracker } from '../../reactive';
2 | import { IObserverOptions } from '../types';
3 | import { useCompatFactory } from './useCompatFactory';
4 | import { useForceUpdate } from './useForceUpdate';
5 |
6 | export const useObserver = any>(view: T, options?: IObserverOptions): ReturnType => {
7 | const forceUpdate = useForceUpdate();
8 | const tracker = useCompatFactory(
9 | () =>
10 | new Tracker(() => {
11 | if (typeof options?.scheduler === 'function') {
12 | options.scheduler(forceUpdate);
13 | } else {
14 | forceUpdate();
15 | }
16 | }, options?.displayName),
17 | );
18 | return tracker.track(view);
19 | };
20 |
--------------------------------------------------------------------------------
/packages/core/src/middlewares/i18n.ts:
--------------------------------------------------------------------------------
1 | import { Locale } from '../locale';
2 |
3 | export async function i18n(ctx, next) {
4 | ctx.getCurrentLocale = () => {
5 | const lng =
6 | ctx.get('X-Locale') ||
7 | (ctx.request.query.locale as string) ||
8 | ctx.tego.i18n.language ||
9 | ctx.acceptsLanguages().shift() ||
10 | 'en-US';
11 | return lng;
12 | };
13 | const lng = ctx.getCurrentLocale();
14 | const localeManager = ctx.tego.localeManager as Locale;
15 | const i18n = await localeManager.getI18nInstance(lng);
16 | ctx.i18n = i18n;
17 | ctx.t = i18n.t.bind(i18n);
18 | if (lng !== '*' && lng) {
19 | i18n.changeLanguage(lng);
20 | await localeManager.loadResourcesByLang(lng);
21 | }
22 | await next();
23 | }
24 |
--------------------------------------------------------------------------------
/packages/di/src/error/cannot-inject-value.error.ts:
--------------------------------------------------------------------------------
1 | import { Constructable } from '@tachybase/utils';
2 |
3 | /**
4 | * Thrown when DI cannot inject value into property decorated by @Inject decorator.
5 | */
6 | export class CannotInjectValueError extends Error {
7 | public name = 'CannotInjectValueError';
8 |
9 | get message(): string {
10 | return (
11 | `Cannot inject value into "${this.target.constructor.name}.${this.propertyName}". ` +
12 | `Please make sure you setup reflect-metadata properly and you don't use interfaces without service tokens as injection value.`
13 | );
14 | }
15 |
16 | constructor(
17 | private target: Constructable,
18 | private propertyName: string,
19 | ) {
20 | super();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/packages/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tego/server",
3 | "version": "1.6.1",
4 | "description": "",
5 | "license": "Apache-2.0",
6 | "main": "./lib/index.js",
7 | "types": "./lib/index.d.ts",
8 | "dependencies": {
9 | "@tachybase/acl": "workspace:*",
10 | "@tachybase/actions": "workspace:*",
11 | "@tachybase/auth": "workspace:*",
12 | "@tachybase/cache": "workspace:*",
13 | "@tachybase/data-source": "workspace:*",
14 | "@tachybase/database": "workspace:*",
15 | "@tachybase/di": "workspace:*",
16 | "@tachybase/evaluators": "workspace:*",
17 | "@tachybase/logger": "workspace:*",
18 | "@tachybase/resourcer": "workspace:*",
19 | "@tachybase/utils": "workspace:*",
20 | "@tego/core": "workspace:*"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/packages/logger/src/config.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 | import TachybaseGlobal from '@tachybase/globals';
3 |
4 | export const getLoggerLevel = () => TachybaseGlobal.settings.logger.level ?? 'info';
5 |
6 | export const getLoggerFilePath = (...paths: string[]): string => {
7 | return path.resolve(
8 | path.resolve(process.env.TEGO_RUNTIME_HOME, TachybaseGlobal.settings.logger.basePath ?? 'storage/logs'),
9 | ...paths,
10 | );
11 | };
12 |
13 | export const getLoggerTransport = (): ('console' | 'file' | 'dailyRotateFile')[] =>
14 | TachybaseGlobal.settings.logger.transport ?? ['console', 'dailyRotateFile'];
15 |
16 | export const getLoggerFormat = (): 'logfmt' | 'json' | 'delimiter' | 'console' =>
17 | TachybaseGlobal.settings.logger.format ?? 'console';
18 |
--------------------------------------------------------------------------------
/packages/schema/src/shared/string.ts:
--------------------------------------------------------------------------------
1 | // ansiRegex
2 | const ansiRegex = () => {
3 | const pattern = [
4 | '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\\u0007)',
5 | '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))',
6 | ].join('|');
7 |
8 | return new RegExp(pattern, 'g');
9 | };
10 |
11 | // astralRegex
12 | const regex = '[\uD800-\uDBFF][\uDC00-\uDFFF]';
13 |
14 | const astralRegex = (opts?: { exact: boolean }) =>
15 | opts && opts.exact ? new RegExp(`^${regex}$`) : new RegExp(regex, 'g');
16 |
17 | // stripAnsi
18 | const stripAnsi = (input: any) => (typeof input === 'string' ? input.replace(ansiRegex(), '') : input);
19 |
20 | export const stringLength = (input: string) => stripAnsi(input).replace(astralRegex(), ' ').length;
21 |
--------------------------------------------------------------------------------
/packages/schema/src/validator/__tests__/parser.spec.ts:
--------------------------------------------------------------------------------
1 | import { parseValidatorDescriptions } from '../parser';
2 |
3 | describe('parseValidatorDescriptions', () => {
4 | test('basic', () => {
5 | expect(parseValidatorDescriptions('date')).toEqual([{ format: 'date' }]);
6 | const validator = () => {
7 | return '';
8 | };
9 | expect(parseValidatorDescriptions(validator)).toEqual([{ validator }]);
10 | expect(parseValidatorDescriptions(['date'])).toEqual([{ format: 'date' }]);
11 | expect(parseValidatorDescriptions([validator])).toEqual([{ validator }]);
12 | expect(parseValidatorDescriptions({ format: 'date' })).toEqual([{ format: 'date' }]);
13 | expect(parseValidatorDescriptions({ validator })).toEqual([{ validator }]);
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/packages/components/src/tree-select/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect, mapProps, mapReadPretty } from '@tachybase/schema';
3 |
4 | import { LoadingOutlined } from '@ant-design/icons';
5 | import { TreeSelect as AntdTreeSelect } from 'antd';
6 |
7 | import { PreviewText } from '../preview-text';
8 |
9 | // FIXME types
10 | export const TreeSelect: any = connect(
11 | AntdTreeSelect,
12 | mapProps(
13 | {
14 | dataSource: 'treeData',
15 | },
16 | (props, field) => {
17 | return {
18 | ...props,
19 | suffixIcon: field?.['loading'] || field?.['validating'] ? : props.suffixIcon,
20 | };
21 | },
22 | ),
23 | mapReadPretty(PreviewText.TreeSelect),
24 | );
25 |
26 | export default TreeSelect;
27 |
--------------------------------------------------------------------------------
/packages/utils/src/isPortalInBody.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 判断一个 DOM 对象是否是由 createPortal 挂在到了 body 上
3 | * @param domNode DOM 对象
4 | */
5 |
6 | export const isPortalInBody = (dom: Element) => {
7 | while (dom) {
8 | // 如果有 `tb-action` 类名,说明是一个 Action 按钮,其本身已经阻止了冒泡,不需要再次阻止,如果阻止会导致点击无效
9 | if (dom.id === 'root' || dom.classList?.contains('tb-action')) {
10 | return false;
11 | }
12 | dom = dom.parentNode as Element;
13 | }
14 |
15 | // 测试环境下大部分都是直接 render 的组件,是没有以 root 为 ID 的根元素的
16 | if (process.env.__TEST__) {
17 | return false;
18 | }
19 |
20 | if (process.env.NODE_ENV !== 'production') {
21 | if (!document.querySelector('#root')) {
22 | throw new Error(`isPortalInBody: can not find element with id 'root'`);
23 | }
24 | }
25 |
26 | return true;
27 | };
28 |
--------------------------------------------------------------------------------
/packages/components/src/form-item/style/grid.tsx:
--------------------------------------------------------------------------------
1 | import { GenerateStyle } from '../../__builtins__';
2 |
3 | export const getGridStyle: GenerateStyle = (token) => {
4 | const { componentCls } = token;
5 |
6 | const colCls = `${componentCls}-item-col`;
7 |
8 | const genGrid = (grid: number) => {
9 | return {
10 | flex: `0 0 ${(grid / 24) * 100}%`,
11 | maxWidth: `${(grid / 24) * 100}%`,
12 | };
13 | };
14 | const genGrids = () => {
15 | return Array.from({ length: 24 }, (_, i) => {
16 | const gridCls = `${colCls}-${i + 1}`;
17 | return {
18 | [gridCls]: genGrid(i + 1),
19 | };
20 | }).reduce((acc, cur) => ({ ...acc, ...cur }), {});
21 | };
22 |
23 | return {
24 | [componentCls]: {
25 | ...genGrids(),
26 | },
27 | };
28 | };
29 |
--------------------------------------------------------------------------------
/packages/core/src/pub-sub-manager/types.ts:
--------------------------------------------------------------------------------
1 | export interface PubSubManagerOptions {
2 | channelPrefix?: string;
3 | }
4 |
5 | export interface PubSubManagerPublishOptions {
6 | skipSelf?: boolean;
7 | onlySelf?: boolean;
8 | }
9 |
10 | export interface PubSubManagerSubscribeOptions {
11 | debounce?: number;
12 | // 回调的真实调用者
13 | callbackCaller?: any;
14 | }
15 |
16 | export type PubSubCallback = (message: any) => Promise;
17 |
18 | export interface IPubSubAdapter {
19 | isConnected(): Promise | boolean;
20 | connect(): Promise;
21 | close(): Promise;
22 | subscribe(channel: string, callback: PubSubCallback): Promise;
23 | unsubscribe(channel: string, callback: PubSubCallback): Promise;
24 | publish(channel: string, message: string | object): Promise;
25 | }
26 |
--------------------------------------------------------------------------------
/packages/database/src/__tests__/value-parsers/base.test.ts:
--------------------------------------------------------------------------------
1 | import { BaseValueParser as ValueParser } from '../../value-parsers';
2 |
3 | describe('number value parser', () => {
4 | let parser: ValueParser;
5 |
6 | beforeEach(() => {
7 | parser = new ValueParser({}, {});
8 | });
9 |
10 | it('should be converted to an array', () => {
11 | expect(parser.toArr('A/B', '/')).toEqual(['A', 'B']);
12 | expect(parser.toArr('A,B')).toEqual(['A', 'B']);
13 | expect(parser.toArr('A, B')).toEqual(['A', 'B']);
14 | expect(parser.toArr('A, B')).toEqual(['A', 'B']);
15 | expect(parser.toArr('A, B ')).toEqual(['A', 'B']);
16 | expect(parser.toArr('A, B ')).toEqual(['A', 'B']);
17 | expect(parser.toArr('A、 B')).toEqual(['A', 'B']);
18 | expect(parser.toArr('A ,, B')).toEqual(['A', 'B']);
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/packages/components/src/cascader/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect, mapProps, mapReadPretty } from '@tachybase/schema';
3 |
4 | import { LoadingOutlined } from '@ant-design/icons';
5 | import { Cascader as AntdCascader } from 'antd';
6 |
7 | import { PreviewText } from '../preview-text';
8 |
9 | export type { BaseOptionType, DefaultOptionType, FieldNames } from 'rc-cascader';
10 |
11 | export const Cascader = connect(
12 | AntdCascader,
13 | mapProps(
14 | {
15 | dataSource: 'options',
16 | },
17 | (props, field) => {
18 | return {
19 | ...props,
20 | suffixIcon: field?.['loading'] || field?.['validating'] ? : props.suffixIcon,
21 | };
22 | },
23 | ),
24 | mapReadPretty(PreviewText.Cascader),
25 | );
26 |
27 | export default Cascader;
28 |
--------------------------------------------------------------------------------
/packages/devkit/src/cli.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import chalk from 'chalk';
3 | import { Command } from 'commander';
4 | import semver from 'semver';
5 |
6 | import commands from './commands';
7 | import { __dirname } from './constants';
8 | import { genTsConfigPaths, initEnv } from './util';
9 |
10 | import './notify-updates';
11 |
12 | import { createRequire } from 'node:module';
13 |
14 | const require = createRequire(import.meta.url);
15 |
16 | const cli = new Command();
17 |
18 | cli.version(require('../package.json').version);
19 |
20 | initEnv();
21 | genTsConfigPaths();
22 |
23 | commands(cli).then(() => {
24 | if (semver.satisfies(process.version, '<20')) {
25 | console.error(chalk.red('[tachybase cli]: Node.js version must be >= 20'));
26 | process.exit(1);
27 | }
28 |
29 | cli.parse(process.argv);
30 | });
31 |
--------------------------------------------------------------------------------
/packages/test/playwright/tests/auth.setup.ts:
--------------------------------------------------------------------------------
1 | import { expect, test as setup } from '@playwright/test';
2 |
3 | // 保存登录状态,避免每次都要登录
4 | setup('admin', async ({ page }) => {
5 | await page.goto('/');
6 | await page.getByPlaceholder('Username/Email').click();
7 | await page.getByPlaceholder('Username/Email').fill('admin@tachybase.com');
8 | await page.getByPlaceholder('Password').click();
9 | await page.getByPlaceholder('Password').fill('admin123');
10 | await page.getByRole('button', { name: 'Sign in' }).click();
11 |
12 | await expect(page.getByTestId('user-center-button')).toBeVisible();
13 |
14 | // 开启配置状态
15 | await page.evaluate(() => {
16 | localStorage.setItem('TACHYBASE_DESIGNABLE', 'true');
17 | });
18 | await page.context().storageState({
19 | path: process.env.PLAYWRIGHT_AUTH_FILE,
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/packages/schema/src/react/__tests__/shared.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react';
2 |
3 | import { render } from '@testing-library/react';
4 |
5 | export class ErrorBoundary extends Component {
6 | state = {
7 | error: null,
8 | };
9 |
10 | componentDidCatch(error: Error) {
11 | this.setState({
12 | error,
13 | });
14 | }
15 |
16 | render() {
17 | if (this.state.error) {
18 | return {this.state.error.message}
;
19 | }
20 | return {this.props.children};
21 | }
22 | }
23 |
24 | export const expectThrowError = (callback: () => React.ReactElement) => {
25 | const { queryByTestId } = render({callback()});
26 | expect(queryByTestId('error-boundary-message')).toBeVisible();
27 | };
28 |
--------------------------------------------------------------------------------
/packages/schema/src/react/components/VoidField.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { useField, useForm } from '../hooks';
4 | import { useAttach } from '../hooks/useAttach';
5 | import { FieldContext } from '../shared';
6 | import { IVoidFieldProps, JSXComponent } from '../types';
7 | import { ReactiveField } from './ReactiveField';
8 |
9 | export const VoidField = (props: IVoidFieldProps) => {
10 | const form = useForm();
11 | const parent = useField();
12 | const field = useAttach(form.createVoidField({ basePath: parent?.address, ...props }));
13 | return (
14 |
15 | {props.children}
16 |
17 | );
18 | };
19 |
20 | VoidField.displayName = 'VoidField';
21 |
--------------------------------------------------------------------------------
/packages/components/src/select/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect, mapProps, mapReadPretty, ReactFC } from '@tachybase/schema';
3 |
4 | import { LoadingOutlined } from '@ant-design/icons';
5 | import { Select as AntdSelect } from 'antd';
6 | import { SelectProps } from 'antd/lib/select';
7 |
8 | import { PreviewText } from '../preview-text';
9 |
10 | export const Select: ReactFC> = connect(
11 | AntdSelect,
12 | mapProps(
13 | {
14 | dataSource: 'options',
15 | loading: true,
16 | },
17 | (props, field) => {
18 | return {
19 | ...props,
20 | suffixIcon: field?.['loading'] || field?.['validating'] ? : props.suffixIcon,
21 | };
22 | },
23 | ),
24 | mapReadPretty(PreviewText.Select),
25 | );
26 |
27 | export default Select;
28 |
--------------------------------------------------------------------------------
/packages/components/src/transfer/index.tsx:
--------------------------------------------------------------------------------
1 | import { connect, isVoidField, mapProps } from '@tachybase/schema';
2 |
3 | import { Transfer as AntdTransfer } from 'antd';
4 |
5 | export const Transfer = connect(
6 | AntdTransfer,
7 | mapProps(
8 | {
9 | value: 'targetKeys',
10 | },
11 | (props, field) => {
12 | if (isVoidField(field)) return props;
13 | return {
14 | ...props,
15 | dataSource:
16 | field.dataSource?.map((item) => {
17 | return {
18 | ...item,
19 | title: item.title || item.label,
20 | key: item.key || item.value,
21 | };
22 | }) || [],
23 | };
24 | },
25 | ),
26 | );
27 |
28 | Transfer.defaultProps = {
29 | render: (item) => item.title ?? null,
30 | };
31 |
32 | export default Transfer;
33 |
--------------------------------------------------------------------------------
/packages/database/src/operators/jsonb.ts:
--------------------------------------------------------------------------------
1 | import { Op } from 'sequelize';
2 |
3 | import { isPg } from './utils';
4 |
5 | function escapeLike(value: string) {
6 | return value.replace(/[_%]/g, '\\$&');
7 | }
8 |
9 | export default {
10 | $containsJsonbValue(value, ctx) {
11 | if (Array.isArray(value)) {
12 | const conditions = value.map((item) => ({
13 | ['::text']: {
14 | [isPg(ctx) ? Op.iLike : Op.like]: `%${escapeLike(item)}%`,
15 | },
16 | }));
17 | return {
18 | [Op.or]: conditions,
19 | };
20 | }
21 |
22 | return {
23 | // 先将 jsonb 转换为字符串, 实际测试发现这种语法可行
24 | ['::text']: {
25 | // 使用 Op.like 操作符来检查 JSONB 字段是否包含特定的字符串
26 | [isPg(ctx) ? Op.iLike : Op.like]: `%${escapeLike(value)}%`,
27 | },
28 | };
29 | },
30 | } as Record;
31 |
--------------------------------------------------------------------------------
/packages/data-source/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tachybase/data-source",
3 | "version": "1.6.1",
4 | "description": "",
5 | "license": "Apache-2.0",
6 | "main": "./lib/index.js",
7 | "types": "./lib/index.d.ts",
8 | "dependencies": {
9 | "@tachybase/acl": "workspace:*",
10 | "@tachybase/actions": "workspace:*",
11 | "@tachybase/database": "workspace:*",
12 | "@tachybase/resourcer": "workspace:*",
13 | "@tachybase/utils": "workspace:*",
14 | "deepmerge": "^4.3.1",
15 | "koa-compose": "^4.1.0",
16 | "lodash": "4.17.21"
17 | },
18 | "devDependencies": {
19 | "@types/koa": "^2.15.0",
20 | "@types/koa-bodyparser": "^4.3.12",
21 | "@types/lodash": "^4.17.20",
22 | "@types/supertest": "^6.0.3",
23 | "koa": "^2.16.2",
24 | "koa-bodyparser": "4.4.1",
25 | "supertest": "^7.1.4"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/packages/devkit/src/builder/interfaces.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 可构建的项目,可以设计成兼容 pnpm、npm、yarn 等 monorepo
3 | */
4 | export interface IProject {
5 | dir: string;
6 | manifest: {
7 | name: string;
8 | };
9 | }
10 |
11 | export type IBuildContext =
12 | | {
13 | onlyTar: true;
14 | sourcemap?: boolean;
15 | dts?: boolean;
16 | retry?: boolean;
17 | development?: boolean;
18 | tar?: boolean;
19 | }
20 | | {
21 | onlyTar?: false | undefined;
22 | sourcemap: boolean;
23 | dts: boolean;
24 | retry: boolean;
25 | development: boolean;
26 | tar: boolean;
27 | };
28 | export interface IBuildablePackage {
29 | name: string;
30 | dir: string;
31 | context: IBuildContext;
32 | build(): Promise;
33 | }
34 |
35 | export interface IBuilder {
36 | build(): Promise;
37 | }
38 |
--------------------------------------------------------------------------------
/packages/database/src/__tests__/fields/uuid-field.test.ts:
--------------------------------------------------------------------------------
1 | import { mockDatabase } from '../';
2 | import { Database } from '../../database';
3 |
4 | describe('string field', () => {
5 | let db: Database;
6 |
7 | beforeEach(async () => {
8 | db = mockDatabase();
9 | await db.clean({ drop: true });
10 | });
11 |
12 | afterEach(async () => {
13 | await db.close();
14 | });
15 |
16 | it('should create uuid field', async () => {
17 | const Test = db.collection({
18 | name: 'tests',
19 | fields: [
20 | {
21 | name: 'uuid',
22 | type: 'uuid',
23 | },
24 | ],
25 | });
26 |
27 | await Test.sync();
28 | const item = await Test.model.create();
29 |
30 | expect(item['uuid']).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/packages/evaluators/src/client/index.tsx:
--------------------------------------------------------------------------------
1 | import { Registry } from '@tachybase/utils/client';
2 |
3 | import formulajs from './engines/formulajs';
4 | import string from './engines/string';
5 |
6 | export interface Evaluator {
7 | label: string;
8 | tooltip?: string;
9 | link?: string;
10 | evaluate(exp: string, scope?: { [key: string]: any }): any;
11 | }
12 |
13 | export const evaluators = new Registry();
14 |
15 | export { evaluate, appendArrayColumn } from '../utils';
16 |
17 | evaluators.register('formula.js', formulajs);
18 | evaluators.register('string', string);
19 |
20 | export function getOptions() {
21 | return Array.from((evaluators as Registry).getEntities()).reduce(
22 | (result: any[], [value, options]) => result.concat({ value, ...options }),
23 | [],
24 | );
25 | }
26 |
27 | export default evaluators;
28 |
--------------------------------------------------------------------------------
/packages/database/src/__tests__/operator/notIn.test.ts:
--------------------------------------------------------------------------------
1 | import Database from '../../database';
2 | import { mockDatabase } from '../index';
3 |
4 | describe('ne operator', () => {
5 | let db: Database;
6 | let Test;
7 | beforeEach(async () => {
8 | db = mockDatabase({});
9 | await db.clean({ drop: true });
10 |
11 | Test = db.collection({
12 | name: 'tests',
13 | fields: [{ type: 'string', name: 'name' }],
14 | });
15 |
16 | await db.sync();
17 | });
18 |
19 | afterEach(async () => {
20 | await db.close();
21 | });
22 |
23 | it('should notIn with null', async () => {
24 | await db.getRepository('tests').create({});
25 |
26 | const results = await db.getRepository('tests').count({
27 | filter: {
28 | 'name.$notIn': ['123'],
29 | },
30 | });
31 |
32 | expect(results).toEqual(1);
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/packages/database/src/value-parsers/to-one-value-parser.ts:
--------------------------------------------------------------------------------
1 | import { Repository } from '../repository';
2 | import { BaseValueParser } from './base-value-parser';
3 |
4 | export class ToOneValueParser extends BaseValueParser {
5 | async setValue(value: any) {
6 | const dataIndex = this.ctx?.column?.dataIndex || [];
7 | if (Array.isArray(dataIndex) && dataIndex.length < 2) {
8 | this.errors.push(`data index invalid`);
9 | return;
10 | }
11 | const key = this.ctx.column.dataIndex[1];
12 | const repository = this.field.database.getRepository(this.field.target) as Repository;
13 | const instance = await repository.findOne({ filter: { [key]: this.trim(value) } });
14 | if (instance) {
15 | this.value = instance.get(this.field.targetKey || 'id');
16 | } else {
17 | this.errors.push(`"${value}" does not exist`);
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/devkit/src/commands/index.ts:
--------------------------------------------------------------------------------
1 | import { Command } from 'commander';
2 |
3 | import { generateAppDir } from '../util';
4 | import build from './build';
5 | import clean from './clean';
6 | import createNginxConf from './create-nginx-conf';
7 | import createPlugin from './create-plugin';
8 | import dev from './dev';
9 | import e2e from './e2e';
10 | import global from './global';
11 | import install from './install';
12 | import postinstall from './postinstall';
13 | import tar from './tar';
14 | import ui from './ui';
15 | import upgrade from './upgrade';
16 |
17 | export default async (cli: Command) => {
18 | generateAppDir();
19 | global(cli);
20 | createNginxConf(cli);
21 | build(cli);
22 | tar(cli);
23 | dev(cli);
24 | ui(cli);
25 | e2e(cli);
26 | clean(cli);
27 | upgrade(cli);
28 | install(cli);
29 | postinstall(cli);
30 | createPlugin(cli);
31 | };
32 |
--------------------------------------------------------------------------------
/packages/database/src/__tests__/inhertits/inherited-map.test.ts:
--------------------------------------------------------------------------------
1 | import InheritanceMap from '../../inherited-map';
2 |
3 | describe('InheritedMap', () => {
4 | it('should setInherits', () => {
5 | const map = new InheritanceMap();
6 | map.setInheritance('b', 'a');
7 |
8 | const nodeA = map.getNode('a');
9 | const nodeB = map.getNode('b');
10 |
11 | expect(nodeA.children.has(nodeB)).toBe(true);
12 | expect(nodeB.parents.has(nodeA)).toBe(true);
13 |
14 | expect(map.isParentNode('a')).toBe(true);
15 | });
16 |
17 | it('should get deep children', () => {
18 | const map = new InheritanceMap();
19 | map.setInheritance('b', 'a');
20 | map.setInheritance('c', 'b');
21 | map.setInheritance('c1', 'b');
22 | map.setInheritance('d', 'c');
23 |
24 | const children = map.getChildren('a');
25 | expect(children.size).toBe(4);
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/packages/components/src/input/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect, mapProps, mapReadPretty, ReactFC } from '@tachybase/schema';
3 |
4 | import { LoadingOutlined } from '@ant-design/icons';
5 | import { Input as AntdInput } from 'antd';
6 | import type { InputProps } from 'antd/es/input';
7 |
8 | import { PreviewText } from '../preview-text';
9 |
10 | const InternalInput: ReactFC = connect(
11 | AntdInput,
12 | mapProps((props, field) => {
13 | return {
14 | ...props,
15 | suffix: {field?.['loading'] || field?.['validating'] ? : props.suffix},
16 | };
17 | }),
18 | mapReadPretty(PreviewText.Input),
19 | );
20 | const TextArea = connect(AntdInput.TextArea, mapReadPretty(PreviewText.Input));
21 |
22 | export const Input = Object.assign(InternalInput, {
23 | TextArea,
24 | });
25 |
26 | export default Input;
27 |
--------------------------------------------------------------------------------
/packages/core/src/__tests__/main-data-source.test.ts:
--------------------------------------------------------------------------------
1 | import { Application } from '@tego/core';
2 |
3 | describe('MainDataSource', () => {
4 | let app: Application;
5 |
6 | beforeEach(async () => {
7 | app = new Application({
8 | database: {
9 | dialect: 'sqlite',
10 | storage: ':memory:',
11 | logging: false,
12 | },
13 | resourcer: {
14 | prefix: '/api',
15 | },
16 | acl: false,
17 | dataWrapping: false,
18 | registerActions: false,
19 | });
20 | });
21 |
22 | afterEach(async () => {
23 | await app.destroy();
24 | });
25 |
26 | it('should create main data source when create application', async () => {
27 | const dataSourceManager = app.dataSourceManager;
28 | const mainDataSource = dataSourceManager.dataSources.get('main');
29 |
30 | expect(mainDataSource).toBeTruthy();
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/packages/devkit/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 |
3 | import { esbuildPluginFilePathExtensions } from 'esbuild-plugin-file-path-extensions';
4 | import fg from 'fast-glob';
5 | import { defineConfig } from 'tsup';
6 |
7 | const entry = fg.globSync(['src/**'], { cwd: __dirname, absolute: true });
8 |
9 | export default defineConfig({
10 | entry,
11 | tsconfig: path.join(__dirname, 'tsconfig.json'),
12 | outDir: path.join(__dirname, 'lib'),
13 | splitting: false,
14 | silent: true,
15 |
16 | outExtension() {
17 | return {
18 | js: `.mjs`,
19 | };
20 | },
21 | esbuildPlugins: [
22 | esbuildPluginFilePathExtensions({
23 | esm: true,
24 | }),
25 | ],
26 |
27 | format: ['esm'],
28 | sourcemap: false,
29 |
30 | clean: true,
31 | bundle: true,
32 | loader: {
33 | '.d.ts': 'copy',
34 | },
35 | skipNodeModulesBundle: true,
36 | });
37 |
--------------------------------------------------------------------------------
/packages/components/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tachybase/components",
3 | "version": "1.6.1",
4 | "license": "Apache-2.0",
5 | "main": "lib/index.js",
6 | "types": "lib/index.d.ts",
7 | "dependencies": {
8 | "@ant-design/cssinjs": "^1.24.0",
9 | "@ant-design/icons": "^6.0.2",
10 | "@dnd-kit/core": "^6.3.1",
11 | "@dnd-kit/sortable": "^8.0.0",
12 | "@dnd-kit/utilities": "^3.2.2",
13 | "@monaco-editor/react": "^4.7.0",
14 | "@tachybase/schema": "workspace:*",
15 | "antd": "5.22.5",
16 | "classnames": "^2.5.1",
17 | "dayjs": "1.11.13",
18 | "prop-types": "^15.8.1",
19 | "react-dom": "18.3.1",
20 | "react-modal": "^3.16.3",
21 | "react-sticky-box": "^1.0.2"
22 | },
23 | "devDependencies": {
24 | "rc-cascader": "3.31.0",
25 | "rc-input-number": "9.5.0",
26 | "rc-picker": "4.11.3",
27 | "rc-tree-select": "5.27.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/packages/database/src/__tests__/fields/set.test.ts:
--------------------------------------------------------------------------------
1 | import { mockDatabase } from '../';
2 | import { Database } from '../../database';
3 |
4 | describe('set field', () => {
5 | let db: Database;
6 |
7 | beforeEach(async () => {
8 | db = mockDatabase();
9 | await db.clean({ drop: true });
10 | });
11 |
12 | afterEach(async () => {
13 | await db.close();
14 | });
15 |
16 | it('should set Set field', async () => {
17 | const A = db.collection({
18 | name: 'a',
19 | fields: [
20 | {
21 | type: 'set',
22 | name: 'set',
23 | },
24 | ],
25 | });
26 |
27 | await db.sync();
28 |
29 | const a = await A.repository.create({});
30 |
31 | a.set('set', ['a', 'b', 'c', 'a']);
32 |
33 | await a.save();
34 |
35 | const setValue = a.get('set');
36 | expect(setValue).toEqual(['a', 'b', 'c']);
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/packages/devkit/src/commands/create-nginx-conf.ts:
--------------------------------------------------------------------------------
1 | import { readFileSync, writeFileSync } from 'node:fs';
2 | import { resolve } from 'node:path';
3 | import { URL } from 'node:url';
4 |
5 | import { Command } from 'commander';
6 |
7 | const __dirname = new URL('.', import.meta.url).pathname;
8 |
9 | export default (cli: Command) => {
10 | cli.command('create-nginx-conf').action(async (name, options) => {
11 | const file = resolve(__dirname, '../../tachybase.conf.tpl');
12 | const data = readFileSync(file, 'utf-8');
13 | const replaced = data
14 | .replace(/\{\{cwd\}\}/g, '/app/tachybase')
15 | .replace(/\{\{publicPath\}\}/g, process.env.APP_PUBLIC_PATH!)
16 | .replace(/\{\{apiPort\}\}/g, process.env.APP_PORT!);
17 |
18 | const targetFile = resolve(process.env.TEGO_RUNTIME_HOME!, 'storage', 'tachybase.conf');
19 | writeFileSync(targetFile, replaced);
20 | });
21 | };
22 |
--------------------------------------------------------------------------------
/packages/schema/src/react/components/Field.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 |
3 | import { useField, useForm } from '../hooks';
4 | import { FieldContext } from '../shared';
5 | import { IFieldProps, JSXComponent } from '../types';
6 | import { ReactiveField } from './ReactiveField';
7 |
8 | export const Field = (props: IFieldProps) => {
9 | const form = useForm();
10 | const parent = useField();
11 | const field = form.createField({ basePath: parent?.address, ...props });
12 | useEffect(() => {
13 | field?.onMount();
14 | return () => {
15 | field?.onUnmount();
16 | };
17 | }, [field]);
18 | return (
19 |
20 | {props.children}
21 |
22 | );
23 | };
24 |
25 | Field.displayName = 'Field';
26 |
--------------------------------------------------------------------------------
/packages/test/src/__tests__/__snapshots__/omitSomeFields.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`omitSomeFields > should omit key & collectionName 1`] = `
4 | [
5 | {
6 | "description": null,
7 | "fields": [
8 | {
9 | "allowNull": false,
10 | "autoIncrement": true,
11 | "description": null,
12 | "interface": "id",
13 | "name": "id",
14 | "parentKey": null,
15 | "primaryKey": true,
16 | "reverseKey": null,
17 | "type": "bigInt",
18 | "uiSchema": {
19 | "title": "{{t("ID")}}",
20 | "type": "number",
21 | "x-component": "InputNumber",
22 | "x-read-pretty": true,
23 | },
24 | },
25 | ],
26 | "hidden": false,
27 | "inherit": false,
28 | "name": "t_0a1w7khj0y7",
29 | "title": "a",
30 | },
31 | ]
32 | `;
33 |
--------------------------------------------------------------------------------
/.cursor/rules/ai-assistant.md:
--------------------------------------------------------------------------------
1 | # AI Assistant Guide / AI 辅助开发指南
2 |
3 | ## Code Generation / 代码生成
4 | - When generating new components, follow the project's component structure and naming conventions.
5 | - 生成新组件时,遵循项目的组件结构和命名规范
6 | - Automatically add necessary type definitions and imports.
7 | - 自动添加必要的类型定义和导入
8 | - Consider internationalization and accessibility.
9 | - 考虑国际化和可访问性
10 |
11 | ## Code Refactoring / 代码重构
12 | - Maintain API compatibility when refactoring.
13 | - 重构时保持 API 兼容性
14 | - Update related tests and documentation.
15 | - 更新相关测试和文档
16 | - Check dependency relationships.
17 | - 检查依赖关系
18 |
19 | ## Troubleshooting / 问题排查
20 | - Check console errors and logs.
21 | - 查看控制台错误和日志
22 | - Check TypeScript type errors.
23 | - 检查 TypeScript 类型错误
24 | - Verify dependency version compatibility.
25 | - 验证依赖版本兼容性
26 | - Check GitHub Actions workflow status (if applicable).
27 | - 查看 GitHub Actions 工作流状态(如适用)
28 |
29 |
--------------------------------------------------------------------------------
/.github/workflows/test-coverage.yaml:
--------------------------------------------------------------------------------
1 | name: Test Coverage
2 | on:
3 | pull_request:
4 | branches:
5 | - main
6 |
7 | jobs:
8 | test-coverage:
9 | runs-on: ubuntu-22.04
10 | steps:
11 | - name: Checkout code
12 | uses: actions/checkout@v3
13 | - name: Setup pnpm
14 | uses: pnpm/action-setup@v4
15 | - name: Setup Node.js
16 | uses: actions/setup-node@v4
17 | with:
18 | node-version: 20
19 | cache: 'pnpm'
20 | - name: Install
21 | run: pnpm install
22 | - name: Tests
23 | run: pnpm vitest --coverage.enabled true
24 | - name: 'Report Coverage'
25 | # Set if: always() to also generate the report if tests are failing
26 | # Only works if you set `reportOnFailure: true` in your vite config as specified above
27 | if: always()
28 | uses: davelosert/vitest-coverage-report-action@v2
29 |
--------------------------------------------------------------------------------
/packages/actions/src/actions/add.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ArrayFieldRepository,
3 | BelongsToManyRepository,
4 | HasManyRepository,
5 | MultipleRelationRepository,
6 | } from '@tachybase/database';
7 |
8 | import { Context } from '..';
9 | import { getRepositoryFromParams } from '../utils';
10 |
11 | export async function add(ctx: Context, next) {
12 | const repository = getRepositoryFromParams(ctx);
13 |
14 | if (
15 | !(
16 | repository instanceof MultipleRelationRepository ||
17 | repository instanceof HasManyRepository ||
18 | repository instanceof ArrayFieldRepository
19 | )
20 | ) {
21 | return await next();
22 | }
23 | const filterByTk = ctx.action.params.filterByTk || ctx.action.params.filterByTks || ctx.action.params.values;
24 |
25 | await (repository).add(filterByTk);
26 |
27 | ctx.status = 200;
28 |
29 | await next();
30 | }
31 |
--------------------------------------------------------------------------------
/packages/cache/src/__tests__/bloom-filter.test.ts:
--------------------------------------------------------------------------------
1 | import { BloomFilter } from '../bloom-filter';
2 | import { CacheManager } from '../cache-manager';
3 |
4 | describe('bloomFilter', () => {
5 | let bloomFilter: BloomFilter;
6 | let cacheManager: CacheManager;
7 |
8 | beforeEach(async () => {
9 | cacheManager = new CacheManager();
10 | cacheManager.registerStore({ name: 'memory', store: 'memory' });
11 | bloomFilter = await cacheManager.createBloomFilter({ store: 'memory' });
12 | await bloomFilter.reserve('bloom-test', 0.01, 1000);
13 | });
14 |
15 | afterEach(async () => {
16 | await cacheManager.flushAll();
17 | });
18 |
19 | it('should add and check', async () => {
20 | await bloomFilter.add('bloom-test', 'hello');
21 | expect(await bloomFilter.exists('bloom-test', 'hello')).toBeTruthy();
22 | expect(await bloomFilter.exists('bloom-test', 'world')).toBeFalsy();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/packages/core/src/gateway/types.ts:
--------------------------------------------------------------------------------
1 | import http, { IncomingMessage, ServerResponse } from 'node:http';
2 |
3 | import { ApplicationOptions } from '../application';
4 |
5 | export interface Handler {
6 | name: string;
7 | prefix: string;
8 | callback: (req: IncomingMessage, res: ServerResponse) => void;
9 | }
10 | export interface AppSelectorMiddlewareContext {
11 | req: IncomingRequest;
12 | resolvedAppName: string | null;
13 | }
14 | export interface RunOptions {
15 | mainAppOptions: ApplicationOptions;
16 | }
17 | export interface StartHttpServerOptions {
18 | port: number;
19 | host: string;
20 | callback?: (server: http.Server) => void;
21 | }
22 | export type AppSelectorMiddleware = (ctx: AppSelectorMiddlewareContext, next: () => Promise) => void;
23 | export type AppSelector = (req: IncomingRequest) => string | Promise;
24 | export interface IncomingRequest {
25 | url: string;
26 | headers: any;
27 | }
28 |
--------------------------------------------------------------------------------
/packages/schema/src/react/components/ObjectField.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { ObjectField as ObjectFieldType } from '../../core';
4 | import { useField, useForm } from '../hooks';
5 | import { useAttach } from '../hooks/useAttach';
6 | import { FieldContext } from '../shared';
7 | import { IFieldProps, JSXComponent } from '../types';
8 | import { ReactiveField } from './ReactiveField';
9 |
10 | export const ObjectField = (
11 | props: IFieldProps,
12 | ) => {
13 | const form = useForm();
14 | const parent = useField();
15 | const field = useAttach(form.createObjectField({ basePath: parent?.address, ...props }));
16 | return (
17 |
18 | {props.children}
19 |
20 | );
21 | };
22 |
23 | ObjectField.displayName = 'ObjectField';
24 |
--------------------------------------------------------------------------------
/packages/data-source/src/default-actions/proxy-to-repository.ts:
--------------------------------------------------------------------------------
1 | import lodash from 'lodash';
2 |
3 | import { DataSource } from '../data-source';
4 | import { getRepositoryFromParams } from './utils';
5 |
6 | export function proxyToRepository(paramKeys: string[] | ((ctx: any) => object), repositoryMethod: string) {
7 | return async function (ctx, next) {
8 | const repository = getRepositoryFromParams(ctx);
9 | const callObj =
10 | typeof paramKeys === 'function' ? paramKeys(ctx) : { ...lodash.pick(ctx.action.params, paramKeys), context: ctx };
11 | const dataSource: DataSource = ctx.dataSource;
12 |
13 | if (!repository[repositoryMethod]) {
14 | throw new Error(
15 | `Repository can not handle action ${repositoryMethod} for ${ctx.action.resourceName} in ${dataSource.name}`,
16 | );
17 | }
18 |
19 | ctx.body = await repository[repositoryMethod](callObj);
20 | await next();
21 | };
22 | }
23 |
--------------------------------------------------------------------------------
/packages/database/src/fields/json-field.ts:
--------------------------------------------------------------------------------
1 | import { DataTypes } from 'sequelize';
2 |
3 | import { BaseColumnFieldOptions, Field } from './field';
4 |
5 | export class JsonField extends Field {
6 | get dataType() {
7 | const dialect = this.context.database.sequelize.getDialect();
8 | const { jsonb } = this.options;
9 | if (dialect === 'postgres' && jsonb) {
10 | return DataTypes.JSONB;
11 | }
12 | return DataTypes.JSON;
13 | }
14 | }
15 |
16 | export interface JsonFieldOptions extends BaseColumnFieldOptions {
17 | type: 'json';
18 | }
19 |
20 | export class JsonbField extends Field {
21 | get dataType() {
22 | const dialect = this.context.database.sequelize.getDialect();
23 | if (dialect === 'postgres') {
24 | return DataTypes.JSONB;
25 | }
26 | return DataTypes.JSON;
27 | }
28 | }
29 |
30 | export interface JsonbFieldOptions extends BaseColumnFieldOptions {
31 | type: 'jsonb';
32 | }
33 |
--------------------------------------------------------------------------------
/packages/core/src/migrations/20230912193824-package-name-unique.ts:
--------------------------------------------------------------------------------
1 | import { DataTypes } from '@tachybase/database';
2 |
3 | import { Migration } from '../migration';
4 |
5 | export default class extends Migration {
6 | on = 'beforeLoad';
7 | appVersion = '<0.14.0-alpha.2';
8 |
9 | async up() {
10 | const tableNameWithSchema = this.pm.collection.getTableNameWithSchema();
11 | const field = this.pm.collection.getField('packageName');
12 | const exists = await field.existsInDb();
13 | if (exists) {
14 | return;
15 | }
16 | try {
17 | await this.db.sequelize.getQueryInterface().addColumn(tableNameWithSchema, field.columnName(), {
18 | type: DataTypes.STRING,
19 | });
20 | await this.db.sequelize.getQueryInterface().addConstraint(tableNameWithSchema, {
21 | type: 'unique',
22 | fields: [field.columnName()],
23 | });
24 | } catch (error) {
25 | //
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/packages/devkit/src/builder/build/tarPlugin.ts:
--------------------------------------------------------------------------------
1 | import { mkdir } from 'node:fs/promises';
2 | import path, { dirname } from 'node:path';
3 |
4 | import Arborist from '@npmcli/arborist';
5 | import fs from 'fs-extra';
6 | import packlist from 'npm-packlist';
7 | import * as tar from 'tar';
8 |
9 | import { TAR_OUTPUT_DIR } from './constant';
10 | import { PkgLog } from './utils';
11 |
12 | export async function tarPlugin(cwd: string, log: PkgLog) {
13 | log('tar package', cwd);
14 | const arborist = new Arborist({ path: cwd });
15 | const node = await arborist.loadActual();
16 | const files = await packlist(node);
17 | const pkg = fs.readJsonSync(path.join(cwd, 'package.json'));
18 | const tarball = path.join(TAR_OUTPUT_DIR, `${pkg.name}-${pkg.version}.tgz`);
19 | await mkdir(dirname(tarball), { recursive: true });
20 | await tar.c(
21 | {
22 | gzip: true,
23 | file: tarball,
24 | cwd,
25 | },
26 | files,
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/packages/schema/src/validator/__tests__/registry.spec.ts:
--------------------------------------------------------------------------------
1 | import { getLocaleByPath, getValidateLocale, getValidateLocaleIOSCode, setValidateLanguage } from '..';
2 | import locale from '../locale';
3 |
4 | test('getValidateLocaleIOSCode', () => {
5 | expect(getValidateLocaleIOSCode('zh-CN')).toEqual('zh-CN');
6 | expect(getValidateLocaleIOSCode('zh')).toEqual('zh');
7 | expect(getValidateLocaleIOSCode('ZH')).toEqual('zh');
8 | expect(getValidateLocaleIOSCode('cn')).toEqual('zh-CN');
9 | expect(getValidateLocaleIOSCode('en')).toEqual('en');
10 | expect(getValidateLocaleIOSCode('TW')).toEqual('zh-TW');
11 | });
12 |
13 | test('getLocaleByPath', () => {
14 | expect(getLocaleByPath('pattern', 'vi')).toEqual(locale.en.pattern);
15 | expect(getLocaleByPath('pattern')).toEqual(locale.en.pattern);
16 | });
17 |
18 | test('getValidateLocale', () => {
19 | setValidateLanguage('vi');
20 | expect(getValidateLocale('pattern')).toEqual(locale.en.pattern);
21 | });
22 |
--------------------------------------------------------------------------------
/packages/core/src/environment.ts:
--------------------------------------------------------------------------------
1 | import { parse } from '@tachybase/utils';
2 |
3 | import _ from 'lodash';
4 |
5 | export class Environment {
6 | private vars = {};
7 |
8 | setVariable(key: string, value: string) {
9 | this.vars[key] = value;
10 | }
11 |
12 | removeVariable(key: string) {
13 | delete this.vars[key];
14 | }
15 |
16 | getVariablesAndSecrets() {
17 | return this.vars;
18 | }
19 |
20 | getVariables() {
21 | return this.vars;
22 | }
23 |
24 | renderJsonTemplate(template: any, options?: { omit?: string[] }) {
25 | if (options?.omit) {
26 | const omitTemplate = _.omit(template, options.omit);
27 | const parsed = parse(omitTemplate)({
28 | $env: this.vars,
29 | });
30 | for (const key of options.omit) {
31 | _.set(parsed, key, _.get(template, key));
32 | }
33 | return parsed;
34 | }
35 | return parse(template)({
36 | $env: this.vars,
37 | });
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/packages/database/src/__tests__/hooks/afterCreateWithAssociations.test.ts:
--------------------------------------------------------------------------------
1 | import { mockDatabase } from '../';
2 | import { Database } from '../../database';
3 |
4 | describe('afterCreateWithAssociations', () => {
5 | let db: Database;
6 |
7 | beforeEach(async () => {
8 | db = mockDatabase();
9 | await db.clean({ drop: true });
10 | });
11 |
12 | afterEach(async () => {
13 | await db.close();
14 | });
15 |
16 | test('case 1', async () => {
17 | db.collection({
18 | name: 'test',
19 | });
20 | await db.sync();
21 | const repo = db.getRepository('test');
22 | db.on('test.afterCreateWithAssociations', async (model, { transaction }) => {
23 | throw new Error('test error');
24 | });
25 | try {
26 | await repo.create({
27 | values: {},
28 | });
29 | } catch (error) {
30 | console.log(error);
31 | }
32 | const count = await repo.count();
33 | expect(count).toBe(0);
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/packages/core/src/migrations/20230912294620-update-pkg.ts:
--------------------------------------------------------------------------------
1 | import { Migration } from '../migration';
2 | import { PluginManager } from '../plugin-manager';
3 |
4 | export default class extends Migration {
5 | on = 'afterSync'; // 'beforeLoad' or 'afterLoad'
6 | appVersion = '<0.14.0-alpha.2';
7 |
8 | async up() {
9 | const plugins = await this.pm.repository.find();
10 | for (const plugin of plugins) {
11 | const { name } = plugin;
12 | if (plugin.packageName) {
13 | continue;
14 | }
15 | try {
16 | const packageName = await PluginManager.getPackageName(name);
17 | await this.pm.repository.update({
18 | filter: {
19 | name,
20 | },
21 | values: {
22 | packageName,
23 | },
24 | });
25 | this.app.logger.info(`update ${packageName}`);
26 | } catch (error) {
27 | this.app.logger.warn(error.message);
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/packages/schema/src/react/components/ArrayField.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { ArrayField as ArrayFieldType } from '../../core';
4 | import { useField, useForm } from '../hooks';
5 | import { useAttach } from '../hooks/useAttach';
6 | import { FieldContext } from '../shared';
7 | import { IFieldProps, JSXComponent } from '../types';
8 | import { ReactiveField } from './ReactiveField';
9 |
10 | export const ArrayField = (
11 | props: IFieldProps,
12 | ) => {
13 | const form = useForm();
14 | const parent = useField();
15 | const field = useAttach(
16 | form.createArrayField({
17 | basePath: parent?.address,
18 | ...props,
19 | }),
20 | );
21 | return (
22 |
23 | {props.children}
24 |
25 | );
26 | };
27 |
28 | ArrayField.displayName = 'ArrayField';
29 |
--------------------------------------------------------------------------------
/packages/auth/src/base/token-control-service.ts:
--------------------------------------------------------------------------------
1 | export interface TokenPolicyConfig {
2 | tokenExpirationTime: string;
3 | sessionExpirationTime: string;
4 | expiredTokenRenewLimit: string;
5 | }
6 |
7 | type millisecond = number;
8 | export type NumericTokenPolicyConfig = {
9 | [K in keyof TokenPolicyConfig]: millisecond;
10 | };
11 |
12 | export type TokenInfo = {
13 | jti: string;
14 | userId: number;
15 | issuedTime: EpochTimeStamp;
16 | signInTime: EpochTimeStamp;
17 | renewed: boolean;
18 | };
19 |
20 | export type JTIStatus = 'valid' | 'inactive' | 'blocked' | 'missing' | 'renewed' | 'expired';
21 | export interface ITokenControlService {
22 | getConfig(): Promise;
23 | setConfig(config: TokenPolicyConfig): Promise;
24 | renew(jti: string): Promise<{ jti: string; issuedTime: EpochTimeStamp }>;
25 | add({ userId }: { userId: number }): Promise;
26 | removeSessionExpiredTokens(userId: number): Promise;
27 | }
28 |
--------------------------------------------------------------------------------
/packages/core/src/__tests__/app-supervisor.test.ts:
--------------------------------------------------------------------------------
1 | import { AppSupervisor } from '../app-supervisor';
2 |
3 | describe('App Supervisor', () => {
4 | let appSupervisor: AppSupervisor;
5 |
6 | beforeEach(() => {
7 | appSupervisor = AppSupervisor.getInstance();
8 | });
9 |
10 | afterEach(async () => {
11 | await appSupervisor.destroy();
12 | });
13 |
14 | it('should get application initializing status', async () => {
15 | expect(appSupervisor.getAppStatus('test')).toBe(undefined);
16 |
17 | appSupervisor.setAppBootstrapper(async () => {
18 | await new Promise((resolve) => setTimeout(resolve, 1000));
19 | });
20 |
21 | appSupervisor.getApp('test');
22 |
23 | await new Promise((resolve) => setTimeout(resolve, 500));
24 | expect(appSupervisor.getAppStatus('test')).toBe('initializing');
25 |
26 | await new Promise((resolve) => setTimeout(resolve, 2000));
27 | expect(appSupervisor.getAppStatus('test')).toBe('not_found');
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/packages/schema/src/reactive/checkers.ts:
--------------------------------------------------------------------------------
1 | const toString = Object.prototype.toString;
2 | export const isMap = (val: any): val is Map => val && val instanceof Map;
3 | export const isSet = (val: any): val is Set => val && val instanceof Set;
4 | export const isWeakMap = (val: any): val is WeakMap => val && val instanceof WeakMap;
5 | export const isWeakSet = (val: any): val is WeakSet => val && val instanceof WeakSet;
6 | export const isFn = (val: any): val is Function => typeof val === 'function';
7 | export const isArr = Array.isArray;
8 | export const isPlainObj = (val: any): val is object => toString.call(val) === '[object Object]';
9 | export const isValid = (val: any) => val !== null && val !== undefined;
10 | export const isCollectionType = (target: any) => {
11 | return isMap(target) || isWeakMap(target) || isSet(target) || isWeakSet(target);
12 | };
13 | export const isNormalType = (target: any) => {
14 | return isPlainObj(target) || isArr(target);
15 | };
16 |
--------------------------------------------------------------------------------
/packages/tego/src/__tests__/utils.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest';
2 |
3 | import { convertEnvToSettings } from '../utils';
4 |
5 | describe('convertEnvToSettings', () => {
6 | it('should convert flat env into structured settings object', () => {
7 | const input = {
8 | LOGGER_TRANSPORT: 'console,dailyRotateFile',
9 | LOGGER_MAX_FILES: '7d',
10 | DB_STORAGE: 'storage/db/tachybase.sqlite',
11 | CACHE_DEFAULT_STORE: 'memory',
12 | INIT_APP_LANG: 'zh-CN',
13 | };
14 |
15 | const result = convertEnvToSettings(input as any);
16 |
17 | expect(result.logger.transport).toEqual(['console', 'dailyRotateFile']);
18 | expect(result.logger.max_files).toBeUndefined(); // 未提供
19 | expect(result.logger.maxFiles).toBe('7d');
20 | expect(result.database.storage).toBe('storage/db/tachybase.sqlite');
21 | expect(result.cache.default_store).toBe('memory');
22 | expect(result.env.INIT_APP_LANG).toBe('zh-CN');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/packages/utils/src/index.ts:
--------------------------------------------------------------------------------
1 | import './dayjs';
2 |
3 | export * from './assign';
4 | export * from './collections-graph';
5 | export * from './common';
6 | export * from './date';
7 | export * from './forEach';
8 | export * from './fs-exists';
9 | export * from './json-templates';
10 | export * from './koa-multer';
11 | export * from './measure-execution-time';
12 | export * from './merge';
13 | export * from './mixin';
14 | export * from './mixin/AsyncEmitter';
15 | export * from './number';
16 | export * from './parse-date';
17 | export * from './parse-filter';
18 | export * from './perf-hooks';
19 | export * from './registry';
20 | export * from './requireModule';
21 | export * from './toposort';
22 | export * from './uid';
23 | export * from './url';
24 | export * from './cluster';
25 | export * from './plugin-symlink';
26 | export * from './currencyUtils';
27 | export * from './getCurrentStacks';
28 | export * from './i18n';
29 | export type { Constructable } from './types/constructable.type';
30 |
--------------------------------------------------------------------------------
/packages/database/src/value-parsers/base-value-parser.ts:
--------------------------------------------------------------------------------
1 | export class BaseValueParser {
2 | ctx: any;
3 | field: any;
4 | value: any;
5 | errors: string[] = [];
6 |
7 | constructor(field: any, ctx: any) {
8 | this.field = field;
9 | this.ctx = ctx;
10 | this.value = null;
11 | }
12 |
13 | trim(value: any) {
14 | return typeof value === 'string' ? value.trim() : value;
15 | }
16 |
17 | toArr(value: any, splitter?: string) {
18 | let values: string[] = [];
19 | if (!value) {
20 | values = [];
21 | } else if (typeof value === 'string') {
22 | values = value.split(splitter || /,|,|、/);
23 | } else if (Array.isArray(value)) {
24 | values = value;
25 | }
26 | return values.map((v) => this.trim(v)).filter(Boolean);
27 | }
28 |
29 | toString() {
30 | return this.value;
31 | }
32 |
33 | getValue() {
34 | return this.value;
35 | }
36 |
37 | async setValue(value: any) {
38 | this.value = value;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/packages/di/src/index.ts:
--------------------------------------------------------------------------------
1 | import { Container } from './container-instance.class';
2 |
3 | export * from './decorators/inject-many.decorator';
4 | export * from './decorators/inject.decorator';
5 | export * from './decorators/service.decorator';
6 |
7 | export * from './error/cannot-inject-value.error';
8 | export * from './error/cannot-instantiate-value.error';
9 | export * from './error/service-not-found.error';
10 |
11 | export type { Handler } from './interfaces/handler.interface';
12 | export type { ServiceMetadata } from './interfaces/service-metadata.interface';
13 | export type { ServiceOptions } from './interfaces/service-options.interface';
14 | export type { ServiceIdentifier } from './types/service-identifier.type';
15 |
16 | export { ContainerInstance, Container } from './container-instance.class';
17 | export { ContainerRegistry } from './container-registry.class';
18 | export { Token } from './token.class';
19 |
20 | export default Container;
21 |
22 | export * from './decorators';
23 |
--------------------------------------------------------------------------------
/packages/components/src/select-table/hooks/useSize.tsx:
--------------------------------------------------------------------------------
1 | import { SizeType } from 'antd/es/config-provider/SizeContext';
2 |
3 | interface ISize {
4 | (
5 | fieldSize: SizeType,
6 | searchSize: SizeType,
7 | tableSize: SizeType,
8 | ): {
9 | searchSize: SizeType;
10 | tableSize: SizeType;
11 | };
12 | }
13 |
14 | const useSize: ISize = (fieldSize = 'middle', searchSize, tableSize) => {
15 | const fieldSizeMap: any = {
16 | small: {
17 | searchSize: 'small',
18 | tableSize: 'small',
19 | },
20 | default: {
21 | searchSize: 'middle',
22 | tableSize: 'middle',
23 | },
24 | large: {
25 | searchSize: 'large',
26 | tableSize: 'default',
27 | },
28 | };
29 | const { searchSize: fieldSearchSize, tableSize: fieldTableSize } = fieldSizeMap[fieldSize] || {};
30 |
31 | return {
32 | searchSize: searchSize || fieldSearchSize,
33 | tableSize: tableSize || fieldTableSize,
34 | };
35 | };
36 |
37 | export { useSize };
38 |
--------------------------------------------------------------------------------
/packages/database/src/__tests__/database.import.test.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 |
3 | import Database from '../database';
4 | import { mockDatabase } from './index';
5 |
6 | describe('database', () => {
7 | let db: Database;
8 |
9 | beforeEach(async () => {
10 | db = mockDatabase();
11 | await db.clean({ drop: true });
12 | });
13 |
14 | afterEach(async () => {
15 | await db.close();
16 | });
17 |
18 | test('import', async () => {
19 | await db.import({
20 | directory: path.resolve(__dirname, './fixtures/c0'),
21 | });
22 | await db.import({
23 | directory: path.resolve(__dirname, './fixtures/c1'),
24 | });
25 | await db.import({
26 | directory: path.resolve(__dirname, './fixtures/c2'),
27 | });
28 |
29 | const test = db.getCollection('tests');
30 |
31 | expect(test.getField('n0')).toBeDefined();
32 | expect(test.getField('n1')).toBeDefined();
33 | expect(test.getField('n2')).toBeDefined();
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/packages/schema/src/core/__tests__/object.spec.ts:
--------------------------------------------------------------------------------
1 | import { createForm } from '..';
2 | import { attach } from './shared';
3 |
4 | test('create object field', () => {
5 | const form = attach(createForm());
6 | const object = attach(
7 | form.createObjectField({
8 | name: 'object',
9 | }),
10 | );
11 | expect(object.value).toEqual({});
12 | expect(object.addProperty).toBeDefined();
13 | expect(object.removeProperty).toBeDefined();
14 | expect(object.existProperty).toBeDefined();
15 | });
16 |
17 | test('create object field methods', () => {
18 | const form = attach(createForm());
19 | const object = attach(
20 | form.createObjectField({
21 | name: 'object',
22 | value: {},
23 | }),
24 | );
25 | expect(object.value).toEqual({});
26 | object.addProperty('aaa', 123);
27 | expect(object.value).toEqual({ aaa: 123 });
28 | object.removeProperty('aaa');
29 | expect(object.value).toEqual({});
30 | expect(object.existProperty('aaa')).toBeFalsy();
31 | });
32 |
--------------------------------------------------------------------------------
/packages/devkit/src/commands/build.ts:
--------------------------------------------------------------------------------
1 | import { type Command } from 'commander';
2 |
3 | import { TachybaseBuilder } from '../builder';
4 |
5 | export default (cli: Command) => {
6 | cli
7 | .command('build')
8 | .allowUnknownOption()
9 | .argument('[packages...]')
10 | .option('-r, --retry', 'retry the last failed package')
11 | .option('-s, --sourcemap', 'generate server sourcemap')
12 | .option('--no-dts', 'not generate dts')
13 | .option('--tar', 'tar the package')
14 | .option('--only-tar', 'only tar the package')
15 | .option('--development', 'development mode')
16 | .action(async (pkgs, options) => {
17 | const tachybaseBuilder = new TachybaseBuilder({
18 | dts: options.dts,
19 | sourcemap: options.sourcemap,
20 | retry: options.retry,
21 | tar: options.tar,
22 | onlyTar: options.onlyTar,
23 | development: options.development,
24 | });
25 |
26 | await tachybaseBuilder.build(pkgs);
27 | });
28 | };
29 |
--------------------------------------------------------------------------------
/packages/components/src/form-item/style/animation.tsx:
--------------------------------------------------------------------------------
1 | import { Keyframes } from '@ant-design/cssinjs';
2 |
3 | import { GenerateStyle } from '../../__builtins__';
4 |
5 | const antShowHelpIn = new Keyframes('antShowHelpIn', {
6 | '0%': {
7 | transform: 'translateY(-5px)',
8 | opacity: 0,
9 | },
10 | to: {
11 | transform: ' translateY(0)',
12 | opacity: 1,
13 | },
14 | });
15 |
16 | export const getAnimationStyle: GenerateStyle = (token) => {
17 | const { componentCls } = token;
18 | const helpCls = `${componentCls}-help`;
19 |
20 | return {
21 | [helpCls]: {
22 | '&-appear, &-enter': {
23 | animationDuration: '0.3s',
24 | animationFillMode: 'both',
25 | animationPlayState: 'paused',
26 | opacity: 0,
27 | animationTimingFunction: 'cubic-bezier(0.645, 0.045, 0.355, 1)',
28 |
29 | '&-active': {
30 | animationPlayState: 'running',
31 | animationName: antShowHelpIn,
32 | },
33 | },
34 | },
35 | };
36 | };
37 |
--------------------------------------------------------------------------------
/packages/database/src/fields/array-field.ts:
--------------------------------------------------------------------------------
1 | import { DataTypes } from 'sequelize';
2 |
3 | import { BaseColumnFieldOptions, Field } from './field';
4 |
5 | export class ArrayField extends Field {
6 | get dataType() {
7 | if (this.database.sequelize.getDialect() === 'postgres') {
8 | return DataTypes.JSONB;
9 | }
10 |
11 | return DataTypes.JSON;
12 | }
13 |
14 | sortValue = (model) => {
15 | let oldValue = model.get(this.options.name);
16 |
17 | if (oldValue) {
18 | if (typeof oldValue === 'string') {
19 | oldValue = JSON.parse(oldValue);
20 | }
21 | const newValue = oldValue.sort();
22 | model.set(this.options.name, newValue);
23 | }
24 | };
25 |
26 | bind() {
27 | super.bind();
28 | this.on('beforeSave', this.sortValue);
29 | }
30 |
31 | unbind() {
32 | super.unbind();
33 | this.off('beforeSave', this.sortValue);
34 | }
35 | }
36 |
37 | export interface ArrayFieldOptions extends BaseColumnFieldOptions {
38 | type: 'array';
39 | }
40 |
--------------------------------------------------------------------------------
/packages/evaluators/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tachybase/evaluators",
3 | "version": "1.6.1",
4 | "description": "",
5 | "license": "Apache-2.0",
6 | "exports": {
7 | ".": {
8 | "require": {
9 | "types": "./lib/index.d.ts",
10 | "default": "./lib/index.js"
11 | },
12 | "import": {
13 | "types": "./lib/index.d.ts",
14 | "default": "./lib/index.js"
15 | }
16 | },
17 | "./client": {
18 | "require": {
19 | "types": "./lib/client/index.d.ts",
20 | "default": "./lib/client/index.js"
21 | },
22 | "import": {
23 | "types": "./lib/client/index.d.ts",
24 | "default": "./lib/client/index.js"
25 | }
26 | }
27 | },
28 | "main": "./lib/index.js",
29 | "types": "./lib/index.d.ts",
30 | "dependencies": {
31 | "@formulajs/formulajs": "4.5.3",
32 | "@tachybase/utils": "workspace:*",
33 | "lodash": "4.17.21"
34 | },
35 | "devDependencies": {
36 | "@types/node": "20.17.10"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/packages/database/src/value-parsers/boolean-value-parser.ts:
--------------------------------------------------------------------------------
1 | import { BaseValueParser } from './base-value-parser';
2 |
3 | export class BooleanValueParser extends BaseValueParser {
4 | async setValue(value: any) {
5 | // Boolean
6 | if (typeof value === 'boolean') {
7 | this.value = value;
8 | }
9 | // Number
10 | else if (typeof value === 'number' && [0, 1].includes(value)) {
11 | this.value = value === 1;
12 | }
13 | // String
14 | else if (typeof value === 'string') {
15 | if (!value) {
16 | this.value = null;
17 | }
18 | if (['1', 'y', 'yes', 'true', '是'].includes(value.toLowerCase())) {
19 | this.value = true;
20 | } else if (['0', 'n', 'no', 'false', '否'].includes(value.toLowerCase())) {
21 | this.value = false;
22 | } else {
23 | this.errors.push(`Invalid value - ${JSON.stringify(this.value)}`);
24 | }
25 | } else {
26 | this.errors.push(`Invalid value - ${JSON.stringify(this.value)}`);
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/packages/data-source/src/default-actions/utils.ts:
--------------------------------------------------------------------------------
1 | import { Context } from '@tachybase/actions';
2 |
3 | import { DataSource, IRepository } from '..';
4 |
5 | export function pageArgsToLimitArgs(
6 | page: number,
7 | pageSize: number,
8 | ): {
9 | offset: number;
10 | limit: number;
11 | } {
12 | return {
13 | offset: (page - 1) * pageSize,
14 | limit: pageSize,
15 | };
16 | }
17 |
18 | export function getRepositoryFromParams(ctx: Context): IRepository {
19 | const { resourceName, sourceId, actionName } = ctx.action;
20 |
21 | const dataSource: DataSource = ctx.dataSource;
22 |
23 | if (sourceId === '_' && ['get', 'list'].includes(actionName)) {
24 | const collection = dataSource.collectionManager.getCollection(resourceName);
25 | return dataSource.collectionManager.getRepository(collection.name);
26 | }
27 |
28 | if (sourceId) {
29 | return dataSource.collectionManager.getRepository(resourceName, sourceId);
30 | }
31 |
32 | return dataSource.collectionManager.getRepository(resourceName);
33 | }
34 |
--------------------------------------------------------------------------------
/.cursorignore:
--------------------------------------------------------------------------------
1 | # Cursor AI 索引忽略文件
2 | # 这些文件和目录不会被 Cursor AI 索引,以提高性能和减少噪音
3 |
4 | # 依赖目录
5 | node_modules/
6 | **/node_modules/
7 | .pnpm-store/
8 |
9 | # 构建产物
10 | dist/
11 | lib/
12 | es/
13 | esm/
14 | **/dist/
15 | **/lib/
16 | **/es/
17 | **/esm/
18 | **/.dumi/tmp
19 | **/.dumi/tmp-production
20 | **/.dumi/tmp-test
21 | **/.turbo
22 | **/.swc
23 | **/.umi
24 | **/.umi-production
25 | docs-dist/
26 | ncc-cache/
27 |
28 | # 测试和覆盖率
29 | coverage/
30 | **/coverage/
31 | playwright/
32 | **/playwright/
33 | *.test.ts.snap
34 | *.spec.ts.snap
35 |
36 | # 日志和临时文件
37 | *.log
38 | *.sqlite
39 | *.tbdump
40 | cache/
41 | v8-compile-cache-**
42 | .DS_Store
43 |
44 | # 存储和上传文件
45 | storage/*
46 | uploads/
47 | docker/**/storage
48 |
49 | # 文档构建产物
50 | docs-repo-temp/
51 |
52 | # 插件临时文件
53 | plugins/*
54 |
55 | # Git 相关
56 | # 注意:COMMIT_EDITMSG 未被忽略,以支持 AI 补全功能
57 | .git/objects/**
58 | .git/refs/**
59 | .git/config
60 | .git/HEAD
61 | .git/index
62 | .git/hooks/**
63 | .git/logs/**
64 | .git/info/**
65 |
66 | # 其他
67 | .local
68 | docs-repo-temp/
69 |
70 |
--------------------------------------------------------------------------------
/packages/database/src/__tests__/fields/nanoid-field.test.ts:
--------------------------------------------------------------------------------
1 | import { mockDatabase } from '../';
2 | import { Database } from '../../database';
3 |
4 | describe('nanoid field', () => {
5 | let db: Database;
6 |
7 | beforeEach(async () => {
8 | db = mockDatabase();
9 | await db.clean({ drop: true });
10 | });
11 |
12 | afterEach(async () => {
13 | await db.close();
14 | });
15 |
16 | it('should create nanoid field type', async () => {
17 | const Test = db.collection({
18 | name: 'tests',
19 | autoGenId: false,
20 | fields: [
21 | {
22 | type: 'nanoid',
23 | name: 'id',
24 | primaryKey: true,
25 | size: 21,
26 | customAlphabet: '1234567890abcdef',
27 | },
28 | {
29 | type: 'nanoid',
30 | name: 'id2',
31 | },
32 | ],
33 | });
34 | await Test.sync();
35 | const test = await Test.model.create();
36 | expect(test.id).toHaveLength(21);
37 | expect(test.id2).toHaveLength(12);
38 | });
39 | });
40 |
--------------------------------------------------------------------------------