diff --git a/.eslintrc.base.js b/.eslintrc.base.js new file mode 100644 index 0000000..005db65 --- /dev/null +++ b/.eslintrc.base.js @@ -0,0 +1,433 @@ +"use strict"; +module.exports = (projectRoot) => ({ + root: true, // fix possible "Plugin %s was conflicted between %s.json and %s.json" errors + env: { + jest: true, + browser: true, + es6: true, + node: true, + }, + extends: [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:import/recommended", + ], + globals: { + Atomics: "readonly", + SharedArrayBuffer: "readonly", + }, + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 2018, + sourceType: "module", + tsconfigRootDir: projectRoot, + project: "tsconfig.json", + warnOnUnsupportedTypeScriptVersion: false, + }, + plugins: [ + "@typescript-eslint", + "import", + "lodash", + "node", + "react-hooks", + "react", + "typescript-enum", + "typescript-sort-keys", + "unused-imports", + ], + settings: { + react: { + version: "detect", + }, + "import/parsers": { + "@typescript-eslint/parser": [".ts", ".tsx"], + }, + "import/resolver": { + typescript: { + project: projectRoot, + }, + }, + }, + ignorePatterns: [ + "node_modules", + "webpack.config.ts", + "**/bin/**", + "*.d.ts", + "**/jest.config.js", + ], + rules: { + // TODO: slowly enable no-extraneous-dependencies rule below. For now, it's + // enforced only for some packages. + // + // In an ideal world, the root package.json should have 0 dependencies, and + // all packages/* should define their own dependencies by themselves, + // independently and locally. The rule below is exactly for that: it ensures + // that all package's dependencies are explicitly mentioned in its + // package.json, and no dependencies are borrowed implicitly from the root + // node_modules. + // + // In real life though, enforcing packages independency is dangerous: we may + // e.g. start accidentally bundle 2 React or 2 Redux versions if we forget + // to sync their versions in different monorepo packages' package.json + // files. (There must be some other lint rule for this hopefully.) + // + // In all cases, we should treat node_modules folders content as something + // secondary and transient. (It's true even now with the new "yarn + // Plug-n-Play" technology which we don't use yet.) The source of truth is + // always package.json (enforced by lint) and yarn.lock (defines the exact + // contents of all node_modules folders, bit by bit). In this schema, it + // doesn't matter at all, does yarn use hoisting or not. + // + // "import/no-extraneous-dependencies": "error"; + + "node/prefer-global/process": "error", + "node/prefer-global/console": "error", + "node/prefer-global/buffer": "error", + "node/prefer-global/url-search-params": "error", + "node/prefer-global/url": "error", + + "require-atomic-updates": "off", + "no-prototype-builtins": "off", + "react/prop-types": "off", + "react/no-unescaped-entities": "off", + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", + "@typescript-eslint/no-misused-promises": "error", + "@typescript-eslint/promise-function-async": "error", + "arrow-body-style": ["error", "as-needed"], + "@typescript-eslint/await-thenable": "error", + "@typescript-eslint/no-floating-promises": ["error", { ignoreVoid: false }], + "@typescript-eslint/unbound-method": ["error", { ignoreStatic: true }], + "@typescript-eslint/return-await": ["error"], + "@typescript-eslint/array-type": ["error", { default: "array-simple" }], + "@typescript-eslint/ban-ts-comment": ["error"], + "@typescript-eslint/no-useless-constructor": ["error"], + "@typescript-eslint/prefer-optional-chain": ["error"], + "@typescript-eslint/consistent-type-imports": ["error"], + eqeqeq: ["error"], + "object-shorthand": ["error", "always"], + "@typescript-eslint/unbound-method": ["error"], + "@typescript-eslint/no-implicit-any-catch": [ + "error", + { allowExplicitAny: true }, + ], + "typescript-enum/no-const-enum": ["error"], // not supported in SWC + + "@typescript-eslint/naming-convention": [ + "error", + { + selector: "variable", + format: ["camelCase", "PascalCase", "UPPER_CASE"], + leadingUnderscore: "allow", + trailingUnderscore: "allow", + filter: { + regex: "^__webpack", + match: false, + }, + }, + ], + + // Disable in favour of @typescript-eslint/no-unused-vars. + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + args: "all", + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + ignoreRestSiblings: true, + }, + ], + "@typescript-eslint/member-ordering": [ + "error", + { + // + // ATTENTION: the rules here are not simple, mainly because of this: + // https://github.com/typescript-eslint/typescript-eslint/issues/6133 + // + // Besides that, we also want contradictory things, like: + // + // 1. Having constructor close to fields definition (because people + // often define fields in the constructor arguments), although it + // logically should've been below static methods. + // 2. Having all abstract things in the class grouped, irregardless on + // their public/protected/private modifiers. + // + default: [ + "signature", + "call-signature", + + // Typically, class constants (that's why they're on top). + "public-static-field", + "public-static-get", + "public-static-set", + "protected-static-field", + "protected-static-get", + "protected-static-set", + + // All concrete fields. What's interesting is that the order we + // emotionally want here for properties is private-protected-public, + // which is the opposite to the order of methods (which is + // public-protected-private). This is likely because the methods are + // bulky, and properties are lean. + "private-static-field", + "private-instance-field", + "public-instance-field", + "public-abstract-field", + "public-abstract-get", + "public-abstract-set", + + // Protected fields and methods are grouped, because eslint currently + // doesn't distinguish fields assigned with a lambda FROM methods, and + // we often times expose abstract protected overridable lambdas: + // https://github.com/typescript-eslint/typescript-eslint/issues/6133 + "protected-abstract-field", + "protected-abstract-get", + "protected-abstract-set", + "protected-abstract-method", + "public-abstract-method", // the only exception; it's to group all abstract things too + "protected-instance-field", + "protected-constructor", + "protected-static-method", + "protected-instance-get", + "protected-instance-set", + "protected-instance-method", + + // Public constructor, instance methods, static methods. + "public-constructor", // often defines more public/protected/private properties, so should be close to fields + "public-static-method", + "public-instance-get", + "public-instance-set", + "public-instance-method", + + // Private constructor, instance methods, static methods. + "private-constructor", + "private-static-method", + "private-instance-get", + "private-instance-set", + "private-instance-method", + "private-static-get", + "private-static-set", + ], + }, + ], + + "no-constant-condition": ["error", { checkLoops: false }], + "no-buffer-constructor": ["error"], + "no-console": ["error"], + curly: ["error", "all"], + "no-case-declarations": "off", + + "padding-line-between-statements": "off", + "@typescript-eslint/padding-line-between-statements": [ + "error", + // Force empty lines. + { + blankLine: "always", + prev: ["block", "block-like", "function", "class", "interface", "type"], + next: "*", + }, + { + blankLine: "always", + prev: "import", + next: [ + "const", + "if", + "let", + "var", + "export", + "function", + "class", + "interface", + "type", + ], + }, + { + blankLine: "always", + prev: "*", + next: ["function", "class", "interface", "type"], + }, + // Allow one-liner functions without extra spacing (hacky): + { blankLine: "any", prev: "singleline-const", next: "*" }, + { blankLine: "any", prev: "singleline-var", next: "*" }, + { blankLine: "any", prev: "singleline-let", next: "*" }, + ], + + "no-restricted-properties": [ + "error", + { + object: "window", + property: "location", + message: + "We use React Router and History to control the location of our web or desktop app. Prefer `useLocation` in React components and `historyFromContext` in Redux Saga.", + }, + ...(projectRoot.endsWith("client") + ? [ + { + object: "window", + property: "document", + message: "Please use `useDocument` from `useDocument.tsx`.", + }, + ...[ + "addEventListener", + "removeEventListener", + "getElementById", + "documentElement", + "activeElement", + "querySelectorAll", + ].map((property) => ({ + object: "document", + property, + message: "Please use `useDocument` from `useDocument.tsx`.", + })), + ] + : []), + ], + + "no-restricted-globals": [ + "warn", + { + name: "location", + message: + "We use React Router and History to control the location of our web or desktop app. Prefer `useLocation` in React components and `historyFromContext` in Redux Saga.", + }, + ], + + "no-restricted-syntax": [ + "error", + { + selector: (() => { + const RE_BAD = "/([a-z0-9_ ]|^)E[Ii][Dd]|(^|[-_: ])eid/"; + return [ + `Identifier[name=${RE_BAD}]`, + `Literal[value=${RE_BAD}]`, + `TemplateElement[value.raw=${RE_BAD}]`, + `TSInterfaceDeclaration[id.name=${RE_BAD}]`, + ].join(","); + })(), + message: + 'Do not use "eid" or "EID" as a part of a name/field/type. Instead, prefer externalID or external_id.', + }, + ], + + "prefer-const": [ + "error", + { + destructuring: "all", + }, + ], + + "no-var": "error", + "no-void": "error", + + "react/forbid-dom-props": [ + "error", + { + forbid: [ + { + propName: "style", + message: "Please use CSS Modules instead", + }, + ], + }, + ], + "react/forbid-component-props": [ + "error", + { + forbid: [ + { + propName: "style", + message: "Please use CSS Modules instead", + }, + ], + }, + ], + "no-sequences": ["error"], + // Too noisy about `react` and other node_modules + "import/default": 0, + // This complains about React.forwardRef, ReactDOM.render, etc. + "import/no-named-as-default-member": 0, + // This complains about "apollo" exporting ApolloClient as a default and as a + // named import at the same time. + "import/no-named-as-default": 0, + // Does not seem to work well with node_modules + "import/named": 0, + "import/newline-after-import": "error", + "import/order": [ + "error", + { + groups: ["builtin", "external", "index", "parent", "sibling"], + pathGroups: [ + { + pattern: "./**.module.css", + group: "sibling", + position: "after", + }, + { + pattern: "./**.module.scss", + group: "sibling", + position: "after", + }, + ], + alphabetize: { + order: "asc", + caseInsensitive: true, + }, + }, + ], + "unused-imports/no-unused-imports": "error", + "no-restricted-imports": [ + "error", + { + patterns: [ + { + group: ["react-router"], + message: + "Please use react-router-dom instead, since react-router's useLocation() doesn't work properly with StaticRouter on server side.", + }, + ], + }, + ], + // Fixes a common mistake: `a ?? b < c` which feels like `(a ?? b) < c`, but + // actually is `a ?? (b < c)` + "no-mixed-operators": [ + "error", + { + allowSamePrecedence: false, + groups: [ + ["??", "+"], + ["??", "-"], + ["??", "*"], + ["??", "/"], + ["??", "%"], + ["??", "**"], + ["??", "&"], + ["??", "|"], + ["??", "^"], + ["??", "~"], + ["??", "<<"], + ["??", ">>"], + ["??", ">>>"], + ["??", "=="], + ["??", "!="], + ["??", "==="], + ["??", "!=="], + ["??", ">"], + ["??", ">="], + ["??", "<"], + ["??", "<="], + ["??", "&&"], + ["??", "||"], + ["??", "in"], + ["??", "instanceof"], + ], + }, + ], + + quotes: ["error", "double", { avoidEscape: true }], + }, +}); diff --git a/.eslintrc.js b/.eslintrc.js index cecb17b..f242838 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,5 @@ "use strict"; -const config = require("../../.eslintrc.base.js")(__dirname); +const config = require("./.eslintrc.base.js")(__dirname); config.rules["import/no-extraneous-dependencies"] = "error"; config.rules["@typescript-eslint/explicit-function-return-type"] = [ "error", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d4b84c9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: "CI Full Run" +on: + pull_request: + branches: + - main + - grok/*/* + push: + branches: + - main +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: ["20.x"] + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + - run: npm install -g pnpm --force + - run: pnpm install + - run: pnpm run build + - run: pnpm run test + - run: pnpm run test:db + - run: pnpm run lint diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 0000000..eda07d1 --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,36 @@ +# Name of this GitHub Actions workflow. +name: Semgrep + +on: + # Scan changed files in PRs (diff-aware scanning): + pull_request: + branches: ['main'] + + # Schedule the CI job (this method uses cron syntax): + schedule: + - cron: '0 0 * * MON-FRI' + +jobs: + semgrep: + # User definable name of this GitHub Actions job. + name: Scan + # If you are self-hosting, change the following `runs-on` value: + runs-on: ubuntu-latest + + container: + # A Docker image with Semgrep installed. Do not change this. + image: returntocorp/semgrep@sha256:6c7ab81e4d1fd25a09f89f1bd52c984ce107c6ff33affef6ca3bc626a4cc479b + + # Skip any PR created by dependabot to avoid permission issues: + if: (github.actor != 'dependabot[bot]') + + steps: + # Fetch project source with GitHub Actions Checkout. + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + # Run the "semgrep ci" command on the command line of the docker image. + - run: semgrep ci + env: + # Connect to Semgrep Cloud Platform through your SEMGREP_APP_TOKEN. + # Generate a token from Semgrep Cloud Platform > Settings + # and add it to your GitHub secrets. + SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af9aba5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +dist + +node_modules +package-lock.json +pnpm-lock.yaml +yarn.lock +.DS_Store +*.log +*.tmp +*.swp diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..7220835 --- /dev/null +++ b/.npmignore @@ -0,0 +1,13 @@ +__tests__ +.npmrc +tsconfig.tsbuildinfo +.github + +node_modules +package-lock.json +pnpm-lock.yaml +yarn.lock +.DS_Store +*.log +*.tmp +*.swp diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..c52ad5f --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +# Published to https://www.npmjs.com diff --git a/README.md b/README.md index f5ea094..554d112 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# pg-sharding: micro-shards support for PostgreSQL +# @clickup/pg-sharding: micro-shards support for PostgreSQL Each micro-shard is a PG schema with numeric suffix. Micro-shards have the same set of tables with same names; it's up to the higher-level tools to keep the diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..ea94832 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,39 @@ +# Security + +Keeping our clients' data secure is an absolute top priority at ClickUp. Our goal is to provide a secure environment, while also being mindful of application performance and the overall user experience. + +ClickUp believes effective disclosure of security vulnerabilities requires mutual trust, respect, transparency and common good between ClickUp and Security Researchers. Together, our vigilant expertise promotes the continued security and privacy of ClickUp customers, products, and services. + +If you think you've found a security vulnerability in any ClickUp-owned repository, please let us know as outlined below. + +ClickUp defines a security vulnerability as an unintended weakness or exposure that could be used to compromise the integrity, availability or confidentiality of our products and services. + +## Our Commitment to Reporters + +- **Trust**. We maintain trust and confidentiality in our professional exchanges with security researchers. +- **Respect**. We treat all researchers with respect and recognize your contribution for keeping our customers safe and secure. +- **Transparency**. We will work with you to validate and remediate reported vulnerabilities in accordance with our commitment to security and privacy. +- **Common Good**. We investigate and remediate issues in a manner consistent with protecting the safety and security of those potentially affected by a reported vulnerability. + +## What We Ask of Reporters + +- **Trust**. We request that you communicate about potential vulnerabilities in a responsible manner, providing sufficient time and information for our team to validate and address potential issues. +- **Respect**. We request that researchers make every effort to avoid privacy violations, degradation of user experience, disruption to production systems, and destruction of data during security testing. +- **Transparency**. We request that researchers provide the technical details and background necessary for our team to identify and validate reported issues, using the form below. +- **Common Good**. We request that researchers act for the common good, protecting user privacy and security by refraining from publicly disclosing unverified vulnerabilities until our team has had time to validate and address reported issues. + +## Vulnerability Reporting + +ClickUp recommends that you share the details of any suspected vulnerabilities across any asset owned, controlled, or operated by ClickUp (or that would reasonably impact the security of ClickUp and our users) using our vulnerability disclosure form at . The ClickUp Security team will acknowledge receipt of each valid vulnerability report, conduct a thorough investigation, and then take appropriate action for resolution. + +## Safe Harbor + +When conducting vulnerability research according to this policy, we consider this research to be: + +- Authorized in accordance with the Computer Fraud and Abuse Act (CFAA) (and/or similar state laws), and we will not initiate or support legal action against you for accidental, good faith violations of this policy; +- Exempt from the Digital Millennium Copyright Act (DMCA), and we will not bring a claim against you for circumvention of technology controls; +- Exempt from restrictions in our Terms & Conditions that would interfere with conducting security research, and we waive those restrictions on a limited basis for work done under this policy; and +- Lawful, helpful to the overall security of the Internet, and conducted in good faith. +- You are expected, as always, to comply with all applicable laws. + +If at any time you have concerns or are uncertain whether your security research is consistent with this policy, please inquire via before going any further. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3b31e6b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +version: "3.4" +services: + postgres: + image: postgres:16 + ports: + - "54833:5432" + environment: + POSTGRES_PASSWORD: postgres + PGDATA: /tmp/postgresql + POSTGRES_INITDB_ARGS: "-c max_connections=1000 -c synchronous_commit=off" + healthcheck: + test: "pg_isready -U postgres" + interval: 1s + timeout: 20s + retries: 10 diff --git a/internal/docker-compose-up.sh b/internal/docker-compose-up.sh new file mode 100644 index 0000000..f3cdd92 --- /dev/null +++ b/internal/docker-compose-up.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +docker-compose up --quiet-pull -d + +for i in $(seq 1 20); do + docker-compose ps | grep postgres | grep -q healthy && break + sleep 1 +done diff --git a/jest.config.js b/jest.config.js index 7ba2d4c..822985a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,5 +1,11 @@ "use strict"; - module.exports = { - ...require("../../jest.config.base")(), + roots: ["/src"], + testMatch: ["**/*.test.ts"], + clearMocks: true, + restoreMocks: true, + ...(process.env.IN_JEST_PROJECT ? {} : { forceExit: true }), + transform: { + "\\.ts$": "ts-jest", + }, }; diff --git a/package.json b/package.json index b806a7c..3703262 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,32 @@ { - "name": "pg-sharding", - "description": "Maintains the desired number of PG schemas (shards) and allows to migrate them.", + "name": "@clickup/pg-sharding", + "description": "Micro-shards support for PostgreSQL", "version": "2.10.291", "license": "MIT", - "main": "dist/cli.js", - "types": "dist/cli.d.ts", + "keywords": [ + "postgresql", + "sharding", + "rebalance", + "cluster" + ], + "main": "./dist/cli.js", + "types": "./dist/cli.d.ts", + "exports": "./dist/cli.js", "bin": { "pg-sharding": "./dist/cli.js" }, "scripts": { - "build": "tsc.sh", - "dev": "tight-loop.sh tsc.sh --watch", - "lint": "lint.sh", - "test": "test.sh", - "test:db": "set -e; for f in sql/__tests__/test_*.sql; do echo == $f; echo; yarn psql -f $f; echo; echo; done" + "build": "tsc", + "dev": "tsc --watch --preserveWatchOutput", + "lint": "eslint . --ext .ts --cache --cache-location dist/.eslintcache", + "test": "set -e; bash internal/docker-compose-up.sh; export PGHOST=127.0.0.1 PGPORT=54833 PGDATABASE=postgres PGUSER=postgres PGPASSWORD=postgres; jest", + "test:db": "set -e; bash internal/docker-compose-up.sh; export PGHOST=127.0.0.1 PGPORT=54833 PGDATABASE=postgres PGUSER=postgres PGPASSWORD=postgres; for f in sql/__tests__/test_*.sql; do echo == $f; echo; psql -f $f; echo; echo; done", + "psql": "PGHOST=127.0.0.1 PGPORT=54833 PGDATABASE=postgres PGUSER=postgres PGPASSWORD=postgres psql", + "docs": "echo No docs step.", + "clean": "rm -rf dist node_modules yarn.lock package-lock.json pnpm-lock.yaml *.log", + "copy-package-to-public-dir": "copy-package-to-public-dir.sh", + "backport-package-from-public-dir": "backport-package-from-public-dir.sh", + "deploy": "npm run build && npm run lint && npm run test && npm publish --access=public" }, "dependencies": { "chalk": "^4.1.2", @@ -25,11 +38,32 @@ "prompts": "^2.4.2", "sprintf-js": "^1.1.2" }, + "devDependencies": { + "@types/jest": "^29.5.5", + "@types/lodash": "^4.14.175", + "@types/minimist": "^1.2.2", + "@types/node": "^20.4.1", + "@types/pg": "^8.6.1", + "@types/prompts": "^2.4.0", + "@types/sprintf-js": "^1.1.2", + "@typescript-eslint/eslint-plugin": "^5.59.6", + "@typescript-eslint/parser": "^5.59.6", + "eslint-import-resolver-typescript": "^3.5.5", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-lodash": "^7.4.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-typescript-enum": "^2.1.0", + "eslint-plugin-typescript-sort-keys": "^2.3.0", + "eslint-plugin-unused-imports": "^2.0.0", + "eslint": "^8.40.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "typescript": "^5.2.2" + }, "repository": { "type": "git", - "url": "git://github.com/time-loop/github-packages" - }, - "publishConfig": { - "registry": "https://npm.pkg.github.com/" + "url": "git://github.com/clickup/pg-sharding" } } diff --git a/sql/__tests__/begin.sql b/sql/__tests__/begin.sql index 49dcb27..aa3c1b2 100644 --- a/sql/__tests__/begin.sql +++ b/sql/__tests__/begin.sql @@ -16,6 +16,9 @@ $$; BEGIN; +CREATE EXTENSION IF NOT EXISTS postgres_fdw; +CREATE EXTENSION IF NOT EXISTS dblink; + CREATE SCHEMA test_sharding; SET search_path TO test_sharding; SET client_min_messages TO NOTICE; diff --git a/sql/functions/sharding_ensure_absent.sql b/sql/functions/sharding_ensure_absent.sql index 9b42458..430bc4e 100644 --- a/sql/functions/sharding_ensure_absent.sql +++ b/sql/functions/sharding_ensure_absent.sql @@ -19,6 +19,7 @@ BEGIN ) SELECT shards.shard FROM shards WHERE EXISTS (SELECT 1 FROM pg_catalog.pg_namespace WHERE nspname = shards.shard) + ORDER BY shards.shard LOOP EXECUTE format('DROP SCHEMA %I', shard); RETURN NEXT shard; diff --git a/sql/functions/sharding_ensure_exist.sql b/sql/functions/sharding_ensure_exist.sql index db8099b..888d1dd 100644 --- a/sql/functions/sharding_ensure_exist.sql +++ b/sql/functions/sharding_ensure_exist.sql @@ -19,6 +19,7 @@ BEGIN ) SELECT shards.shard FROM shards WHERE NOT EXISTS (SELECT 1 FROM pg_catalog.pg_namespace WHERE nspname = shards.shard) + ORDER BY shards.shard LOOP EXECUTE format('CREATE SCHEMA %I', shard); RETURN NEXT shard::regnamespace; diff --git a/sql/functions/sharding_list_active_shards.sql b/sql/functions/sharding_list_active_shards.sql index c74db77..15045b5 100644 --- a/sql/functions/sharding_list_active_shards.sql +++ b/sql/functions/sharding_list_active_shards.sql @@ -4,8 +4,7 @@ SET search_path FROM CURRENT AS $$ SELECT COALESCE(array_agg(nspname::regnamespace ORDER BY nspname), '{}') FROM pg_namespace - WHERE - pg_namespace.nspname = ANY(_sharding_active_shards()) + WHERE pg_namespace.nspname = ANY(_sharding_active_shards()) $$; COMMENT ON FUNCTION sharding_list_active_shards() diff --git a/src/cli.ts b/src/cli.ts index 7db7e6d..720cb75 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -5,8 +5,9 @@ import cleanup from "./api/cleanup"; import move from "./api/move"; import rebalance from "./api/rebalance"; import { log } from "./helpers/logging"; +import shellQuote from "./helpers/shellQuote"; -export { cleanup, move, rebalance }; +export { cleanup, move, rebalance, shellQuote }; const USAGE = [ "Usage:", @@ -25,22 +26,22 @@ export async function main(argv: string[]): Promise { if (args._[0] === "move") { let shard: number; - if ((args.shard ?? "").match(/^(\d+)$/)) { - shard = parseInt(args.shard); + if ((args["shard"] ?? "").match(/^(\d+)$/)) { + shard = parseInt(args["shard"]); } else { throw "Please provide --shard, a numeric shard number to move"; } let fromDsn: string; - if ((args.from ?? "").match(/^\w+:\/\//)) { - fromDsn = args.from; + if ((args["from"] ?? "").match(/^\w+:\/\//)) { + fromDsn = args["from"]; } else { throw "Please provide --from, source DB DSN, as postgresql://user:pass@host/db?options"; } let toDsn: string; - if ((args.to ?? "").match(/^\w+:\/\//)) { - toDsn = args.to; + if ((args["to"] ?? "").match(/^\w+:\/\//)) { + toDsn = args["to"]; } else { throw "Please provide --to, destination DB DSN, as postgresql://user:pass@host/db?options"; } @@ -74,8 +75,8 @@ export async function main(argv: string[]): Promise { if (args._[0] === "cleanup") { let dsn: string; - if ((args.dsn ?? "").match(/^\w+:\/\//)) { - dsn = args.dsn; + if ((args["dsn"] ?? "").match(/^\w+:\/\//)) { + dsn = args["dsn"]; } else { throw "Please provide --dsn, DB DSN to remove old schemas from, as postgresql://user:pass@host/db?options"; } diff --git a/src/helpers/runShell.ts b/src/helpers/runShell.ts index 239e322..8411fbf 100644 --- a/src/helpers/runShell.ts +++ b/src/helpers/runShell.ts @@ -33,7 +33,7 @@ export default async function runShell( ...process.env, PGOPTIONS: compact([ "--client-min-messages=warning", - process.env.PGOPTIONS, + process.env["PGOPTIONS"], ]).join(" "), }, }) diff --git a/tsconfig.json b/tsconfig.json index e671436..65ddd6e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,32 @@ { - "extends": "../../tsconfig.base.json", "include": ["src/**/*"], "compilerOptions": { - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo", - "emitDeclarationOnly": true, // turns on SWC - "rootDir": "src", + "allowJs": true, + "declaration": true, + "declarationMap": true, + "disableReferencedProjectLoad": true, + "disableSourceOfProjectReferenceRedirect": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "incremental": true, + "lib": ["es2019"], + "module": "commonjs", + "moduleResolution": "node", + "noEmitOnError": true, + "noErrorTruncation": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": true, "outDir": "dist", - "lib": ["es2019", "DOM"], - "types": ["node", "jest"], - "module": "commonjs" + "pretty": true, + "removeComments": false, + "resolveJsonModule": true, + "rootDir": "src", + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "es2019", + "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo", + "types": ["node", "jest"] } }