From 9d8e3d8f2614af0e5a2032d3548d88eb2a6cc821 Mon Sep 17 00:00:00 2001
From: Matt <1009003+tantaman@users.noreply.github.com>
Date: Thu, 9 Nov 2023 15:09:10 -0500
Subject: [PATCH 01/25] re-write to use the row versioning strategy and rails.
This was a bit of a scorched earth swap.
Swapped from nextjs -> vite and based this off of https://github.com/rocicorp/todo-row-versioning/tree/main
---
.babelrc | 14 -
.eslintignore | 34 -
.eslintrc | 51 -
.gitignore | 129 +-
.jshintrc | 6 -
.npmrc | 1 -
.prettierignore | 34 -
.prettierrc.json | 8 -
.vscode | 9 -
LICENSE | 26 +-
README.md | 68 +-
backend/comments-react.d.ts | 16 -
backend/data.test.ts | 390 -
backend/data.ts | 457 -
backend/issues-react.d.ts | 15 -
backend/issues-react.js.gz | Bin 4423605 -> 0 bytes
backend/pg.ts | 117 -
backend/replicache-transaction.test.ts | 104 -
backend/replicache-transaction.ts | 143 -
backend/sample-issues.ts | 136 -
backend/sync-order.ts | 38 -
client/.env.example | 1 +
client/.eslintignore | 9 +
client/.npmignore | 25 +
client/.prettierignore | 4 +
client/env.d.ts | 11 +
client/index.html | 16 +
client/package.json | 65 +
client/postcss.config.js | 7 +
{frontend => client/src}/about-modal.tsx | 30 +-
client/src/app.tsx | 236 +
.../src}/assets/fonts/27237475-28043385 | Bin
.../src}/assets/fonts/Inter-UI-ExtraBold.woff | Bin
.../assets/fonts/Inter-UI-ExtraBold.woff2 | Bin
.../src}/assets/fonts/Inter-UI-Medium.woff | Bin
.../src}/assets/fonts/Inter-UI-Medium.woff2 | Bin
.../src}/assets/fonts/Inter-UI-Regular.woff | Bin
.../src}/assets/fonts/Inter-UI-Regular.woff2 | Bin
.../src}/assets/fonts/Inter-UI-SemiBold.woff | Bin
.../src}/assets/fonts/Inter-UI-SemiBold.woff2 | Bin
.../src}/assets/icons/add-subissue.svg | 0
{frontend => client/src}/assets/icons/add.svg | 0
.../src}/assets/icons/archive.svg | 0
.../src}/assets/icons/arrow.svg | 0
.../src}/assets/icons/assignee.svg | 0
.../src}/assets/icons/attachment.svg | 0
.../src}/assets/icons/avatar.svg | 0
.../src}/assets/icons/cancel.svg | 0
.../src}/assets/icons/chat.svg | 0
.../src}/assets/icons/circle-dot.svg | 0
.../src}/assets/icons/circle.svg | 0
.../src}/assets/icons/claim.svg | 0
.../src}/assets/icons/close.svg | 0
.../src}/assets/icons/delete.svg | 0
.../src}/assets/icons/done.svg | 0
.../src}/assets/icons/dots.svg | 0
.../src}/assets/icons/due-date.svg | 0
.../src}/assets/icons/dupplication.svg | 0
.../src}/assets/icons/filter.svg | 0
.../src}/assets/icons/git-issue.svg | 0
.../src}/assets/icons/guide.svg | 0
.../src}/assets/icons/half-circle.svg | 0
.../src}/assets/icons/help.svg | 0
.../src}/assets/icons/inbox.svg | 0
.../src}/assets/icons/issue.svg | 0
.../src}/assets/icons/label.svg | 0
.../src}/assets/icons/menu.svg | 0
.../src}/assets/icons/parent-issue.svg | 0
.../src}/assets/icons/plus.svg | 0
.../src}/assets/icons/project.svg | 0
.../src}/assets/icons/question.svg | 0
.../src}/assets/icons/relationship.svg | 0
.../src}/assets/icons/rounded-claim.svg | 0
.../src}/assets/icons/search.svg | 0
.../src}/assets/icons/signal-medium.svg | 0
.../src}/assets/icons/signal-strong.svg | 0
.../src}/assets/icons/signal-strong.xsd | 0
.../src}/assets/icons/signal-weak.svg | 0
.../src}/assets/icons/slack.svg | 0
.../src}/assets/icons/view.svg | 0
.../src}/assets/icons/zoom.svg | 0
.../src}/assets/images/logo.svg | 0
client/src/filters.ts | 99 +
client/src/hooks/query-state-hooks.ts | 42 +
.../src}/hooks/useClickOutside.ts | 0
.../src}/hooks/useKeyPressed.ts | 0
.../src}/hooks/useLockBodyScroll.ts | 0
client/src/hooks/useQueryState.ts | 76 +
client/src/index.css | 56 +
client/src/index.tsx | 39 +
.../src/issue}/issue-board.tsx | 90 +-
{frontend => client/src/issue}/issue-col.tsx | 22 +-
.../src/issue}/issue-detail.tsx | 178 +-
.../src/issue}/issue-item-base.tsx | 10 +-
{frontend => client/src/issue}/issue-item.tsx | 14 +-
{frontend => client/src/issue}/issue-list.tsx | 26 +-
.../src/issue}/issue-modal.tsx | 56 +-
{frontend => client/src/issue}/issue-row.tsx | 12 +-
client/src/issue/issue.ts | 118 +
.../src/layout}/item-group.tsx | 0
client/src/layout/layout.tsx | 123 +
{frontend => client/src/layout}/left-menu.tsx | 69 +-
{frontend => client/src/layout}/modal.tsx | 45 +-
.../src/layout}/top-filter.tsx | 76 +-
client/src/model/control.ts | 12 +
{frontend => client/src/model}/mutators.ts | 47 +-
client/src/reducer.ts | 194 +
{util => client/src/util}/asserts.ts | 0
{util => client/src/util}/date.ts | 0
{frontend => client/src/util}/immutable.ts | 0
{util => client/src/util}/json.ts | 0
client/src/util/sync-lock.ts | 56 +
client/src/vite-env.d.ts | 2 +
.../src/widgets}/filter-menu.tsx | 35 +-
client/src/widgets/priority-icon.tsx | 29 +
.../src/widgets}/priority-menu.tsx | 66 +-
.../src/widgets}/searchbox.tsx | 30 +-
{frontend => client/src/widgets}/select.tsx | 0
.../src/widgets}/sort-order-menu.tsx | 38 +-
client/src/widgets/status-icon.tsx | 35 +
.../src/widgets}/status-menu.tsx | 62 +-
client/tailwind.config.js | 106 +
client/tsconfig.json | 21 +
client/tsconfig.node.json | 9 +
client/vite.config.ts | 18 +
frontend/app.tsx | 626 -
frontend/control.ts | 22 -
frontend/issue.ts | 248 -
frontend/priority-icon.tsx | 30 -
frontend/status-icon.tsx | 35 -
next-env.d.ts | 5 -
next.config.js | 22 -
notes.md | 8 +
package-lock.json | 22020 +++++++++-------
package.json | 95 +-
pages/_app.tsx | 25 -
pages/_document.tsx | 29 -
pages/_middleware.tsx | 33 -
pages/api/replicache-pull.ts | 193 -
pages/api/replicache-push.ts | 168 -
pages/d/[id].tsx | 58 -
pages/index.tsx | 22 -
postcss.config.js | 3 -
public/static/replicache-logo-96.png | Bin 11633 -> 0 bytes
render.yaml | 23 +
server/.eslintignore | 9 +
server/endpoints/handle-poke.ts | 39 +
server/endpoints/replicache-pull.ts | 15 +
server/endpoints/replicache-push.ts | 16 +
server/nodemon.json | 6 +
server/package.json | 54 +
server/src/data.ts | 355 +
server/src/main.ts | 60 +
server/src/pg.ts | 131 +
server/src/pgconfig/pgconfig.ts | 20 +
server/src/pgconfig/pgmem.ts | 20 +
server/src/pgconfig/postgres.ts | 40 +
server/src/poke.ts | 56 +
server/src/pull/cvr.ts | 236 +
server/src/pull/next-page.ts | 189 +
server/src/pull/pull.ts | 263 +
server/src/push.ts | 165 +
server/src/schema.ts | 112 +
.../src/seed/comments-react.json.gz | Bin 8973966 -> 8973959 bytes
server/src/seed/issues-react.json.gz | Bin 0 -> 4423594 bytes
server/src/seed/sample-issues.ts | 199 +
server/src/seed/seed-db.ts | 157 +
tsconfig.json => server/tsconfig.json | 25 +-
shared/.eslintignore | 9 +
shared/package.json | 22 +
shared/src/comment.ts | 19 +
shared/src/description.ts | 15 +
shared/src/entitySchema.ts | 5 +
shared/src/index.ts | 7 +
shared/src/issue.ts | 80 +
shared/src/mutators.ts | 37 +
shared/tsconfig.json | 23 +
styles/index.css | 56 -
supabase/config.toml | 60 -
tailwind.config.js | 106 -
180 files changed, 16550 insertions(+), 13832 deletions(-)
delete mode 100644 .babelrc
delete mode 100644 .eslintignore
delete mode 100644 .eslintrc
delete mode 100644 .jshintrc
delete mode 100644 .npmrc
delete mode 100644 .prettierignore
delete mode 100644 .prettierrc.json
delete mode 100644 .vscode
delete mode 100644 backend/comments-react.d.ts
delete mode 100644 backend/data.test.ts
delete mode 100644 backend/data.ts
delete mode 100644 backend/issues-react.d.ts
delete mode 100644 backend/issues-react.js.gz
delete mode 100644 backend/pg.ts
delete mode 100644 backend/replicache-transaction.test.ts
delete mode 100644 backend/replicache-transaction.ts
delete mode 100644 backend/sample-issues.ts
delete mode 100644 backend/sync-order.ts
create mode 100644 client/.env.example
create mode 100644 client/.eslintignore
create mode 100644 client/.npmignore
create mode 100644 client/.prettierignore
create mode 100644 client/env.d.ts
create mode 100644 client/index.html
create mode 100644 client/package.json
create mode 100644 client/postcss.config.js
rename {frontend => client/src}/about-modal.tsx (81%)
create mode 100644 client/src/app.tsx
rename {frontend => client/src}/assets/fonts/27237475-28043385 (100%)
rename {frontend => client/src}/assets/fonts/Inter-UI-ExtraBold.woff (100%)
rename {frontend => client/src}/assets/fonts/Inter-UI-ExtraBold.woff2 (100%)
rename {frontend => client/src}/assets/fonts/Inter-UI-Medium.woff (100%)
rename {frontend => client/src}/assets/fonts/Inter-UI-Medium.woff2 (100%)
rename {frontend => client/src}/assets/fonts/Inter-UI-Regular.woff (100%)
rename {frontend => client/src}/assets/fonts/Inter-UI-Regular.woff2 (100%)
rename {frontend => client/src}/assets/fonts/Inter-UI-SemiBold.woff (100%)
rename {frontend => client/src}/assets/fonts/Inter-UI-SemiBold.woff2 (100%)
rename {frontend => client/src}/assets/icons/add-subissue.svg (100%)
rename {frontend => client/src}/assets/icons/add.svg (100%)
rename {frontend => client/src}/assets/icons/archive.svg (100%)
rename {frontend => client/src}/assets/icons/arrow.svg (100%)
rename {frontend => client/src}/assets/icons/assignee.svg (100%)
rename {frontend => client/src}/assets/icons/attachment.svg (100%)
rename {frontend => client/src}/assets/icons/avatar.svg (100%)
rename {frontend => client/src}/assets/icons/cancel.svg (100%)
rename {frontend => client/src}/assets/icons/chat.svg (100%)
rename {frontend => client/src}/assets/icons/circle-dot.svg (100%)
rename {frontend => client/src}/assets/icons/circle.svg (100%)
rename {frontend => client/src}/assets/icons/claim.svg (100%)
rename {frontend => client/src}/assets/icons/close.svg (100%)
rename {frontend => client/src}/assets/icons/delete.svg (100%)
rename {frontend => client/src}/assets/icons/done.svg (100%)
rename {frontend => client/src}/assets/icons/dots.svg (100%)
rename {frontend => client/src}/assets/icons/due-date.svg (100%)
rename {frontend => client/src}/assets/icons/dupplication.svg (100%)
rename {frontend => client/src}/assets/icons/filter.svg (100%)
rename {frontend => client/src}/assets/icons/git-issue.svg (100%)
rename {frontend => client/src}/assets/icons/guide.svg (100%)
rename {frontend => client/src}/assets/icons/half-circle.svg (100%)
rename {frontend => client/src}/assets/icons/help.svg (100%)
rename {frontend => client/src}/assets/icons/inbox.svg (100%)
rename {frontend => client/src}/assets/icons/issue.svg (100%)
rename {frontend => client/src}/assets/icons/label.svg (100%)
rename {frontend => client/src}/assets/icons/menu.svg (100%)
rename {frontend => client/src}/assets/icons/parent-issue.svg (100%)
rename {frontend => client/src}/assets/icons/plus.svg (100%)
rename {frontend => client/src}/assets/icons/project.svg (100%)
rename {frontend => client/src}/assets/icons/question.svg (100%)
rename {frontend => client/src}/assets/icons/relationship.svg (100%)
rename {frontend => client/src}/assets/icons/rounded-claim.svg (100%)
rename {frontend => client/src}/assets/icons/search.svg (100%)
rename {frontend => client/src}/assets/icons/signal-medium.svg (100%)
rename {frontend => client/src}/assets/icons/signal-strong.svg (100%)
rename {frontend => client/src}/assets/icons/signal-strong.xsd (100%)
rename {frontend => client/src}/assets/icons/signal-weak.svg (100%)
rename {frontend => client/src}/assets/icons/slack.svg (100%)
rename {frontend => client/src}/assets/icons/view.svg (100%)
rename {frontend => client/src}/assets/icons/zoom.svg (100%)
rename {frontend => client/src}/assets/images/logo.svg (100%)
create mode 100644 client/src/filters.ts
create mode 100644 client/src/hooks/query-state-hooks.ts
rename {frontend => client/src}/hooks/useClickOutside.ts (100%)
rename {frontend => client/src}/hooks/useKeyPressed.ts (100%)
rename {frontend => client/src}/hooks/useLockBodyScroll.ts (100%)
create mode 100644 client/src/hooks/useQueryState.ts
create mode 100644 client/src/index.css
create mode 100644 client/src/index.tsx
rename {frontend => client/src/issue}/issue-board.tsx (66%)
rename {frontend => client/src/issue}/issue-col.tsx (84%)
rename {frontend => client/src/issue}/issue-detail.tsx (70%)
rename {frontend => client/src/issue}/issue-item-base.tsx (78%)
rename {frontend => client/src/issue}/issue-item.tsx (73%)
rename {frontend => client/src/issue}/issue-list.tsx (78%)
rename {frontend => client/src/issue}/issue-modal.tsx (73%)
rename {frontend => client/src/issue}/issue-row.tsx (84%)
create mode 100644 client/src/issue/issue.ts
rename {frontend => client/src/layout}/item-group.tsx (100%)
create mode 100644 client/src/layout/layout.tsx
rename {frontend => client/src/layout}/left-menu.tsx (71%)
rename {frontend => client/src/layout}/modal.tsx (59%)
rename {frontend => client/src/layout}/top-filter.tsx (67%)
create mode 100644 client/src/model/control.ts
rename {frontend => client/src/model}/mutators.ts (62%)
create mode 100644 client/src/reducer.ts
rename {util => client/src/util}/asserts.ts (100%)
rename {util => client/src/util}/date.ts (100%)
rename {frontend => client/src/util}/immutable.ts (100%)
rename {util => client/src/util}/json.ts (100%)
create mode 100644 client/src/util/sync-lock.ts
create mode 100644 client/src/vite-env.d.ts
rename {frontend => client/src/widgets}/filter-menu.tsx (77%)
create mode 100644 client/src/widgets/priority-icon.tsx
rename {frontend => client/src/widgets}/priority-menu.tsx (62%)
rename {frontend => client/src/widgets}/searchbox.tsx (69%)
rename {frontend => client/src/widgets}/select.tsx (100%)
rename {frontend => client/src/widgets}/sort-order-menu.tsx (65%)
create mode 100644 client/src/widgets/status-icon.tsx
rename {frontend => client/src/widgets}/status-menu.tsx (63%)
create mode 100644 client/tailwind.config.js
create mode 100644 client/tsconfig.json
create mode 100644 client/tsconfig.node.json
create mode 100644 client/vite.config.ts
delete mode 100644 frontend/app.tsx
delete mode 100644 frontend/control.ts
delete mode 100644 frontend/issue.ts
delete mode 100644 frontend/priority-icon.tsx
delete mode 100644 frontend/status-icon.tsx
delete mode 100644 next-env.d.ts
delete mode 100644 next.config.js
create mode 100644 notes.md
delete mode 100644 pages/_app.tsx
delete mode 100644 pages/_document.tsx
delete mode 100644 pages/_middleware.tsx
delete mode 100644 pages/api/replicache-pull.ts
delete mode 100644 pages/api/replicache-push.ts
delete mode 100644 pages/d/[id].tsx
delete mode 100644 pages/index.tsx
delete mode 100644 postcss.config.js
delete mode 100644 public/static/replicache-logo-96.png
create mode 100644 render.yaml
create mode 100644 server/.eslintignore
create mode 100644 server/endpoints/handle-poke.ts
create mode 100644 server/endpoints/replicache-pull.ts
create mode 100644 server/endpoints/replicache-push.ts
create mode 100644 server/nodemon.json
create mode 100644 server/package.json
create mode 100644 server/src/data.ts
create mode 100644 server/src/main.ts
create mode 100644 server/src/pg.ts
create mode 100644 server/src/pgconfig/pgconfig.ts
create mode 100644 server/src/pgconfig/pgmem.ts
create mode 100644 server/src/pgconfig/postgres.ts
create mode 100644 server/src/poke.ts
create mode 100644 server/src/pull/cvr.ts
create mode 100644 server/src/pull/next-page.ts
create mode 100644 server/src/pull/pull.ts
create mode 100644 server/src/push.ts
create mode 100644 server/src/schema.ts
rename backend/comments-react.js.gz => server/src/seed/comments-react.json.gz (56%)
create mode 100644 server/src/seed/issues-react.json.gz
create mode 100644 server/src/seed/sample-issues.ts
create mode 100644 server/src/seed/seed-db.ts
rename tsconfig.json => server/tsconfig.json (50%)
create mode 100644 shared/.eslintignore
create mode 100644 shared/package.json
create mode 100644 shared/src/comment.ts
create mode 100644 shared/src/description.ts
create mode 100644 shared/src/entitySchema.ts
create mode 100644 shared/src/index.ts
create mode 100644 shared/src/issue.ts
create mode 100644 shared/src/mutators.ts
create mode 100644 shared/tsconfig.json
delete mode 100644 styles/index.css
delete mode 100644 supabase/config.toml
delete mode 100644 tailwind.config.js
diff --git a/.babelrc b/.babelrc
deleted file mode 100644
index e22cfb0e..00000000
--- a/.babelrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "presets": [
- [
- "next/babel",
- {
- "preset-env": {
- "targets": {
- "esmodules": true
- }
- }
- }
- ]
- ]
-}
diff --git a/.eslintignore b/.eslintignore
deleted file mode 100644
index e3b3fe77..00000000
--- a/.eslintignore
+++ /dev/null
@@ -1,34 +0,0 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-
-# dependencies
-/node_modules
-/.pnp
-.pnp.js
-
-# testing
-/coverage
-
-# next.js
-/.next/
-/out/
-
-# production
-/build
-
-# misc
-.DS_Store
-*.pem
-
-# debug
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-
-# local env files
-.env.local
-.env.development.local
-.env.test.local
-.env.production.local
-
-# vercel
-.vercel
\ No newline at end of file
diff --git a/.eslintrc b/.eslintrc
deleted file mode 100644
index f518d6f0..00000000
--- a/.eslintrc
+++ /dev/null
@@ -1,51 +0,0 @@
-{
- "env": {
- "browser": true,
- "node": true
- },
- "parser": "@typescript-eslint/parser",
- "parserOptions": {
- "ecmaVersion": 12,
- "sourceType": "module",
- "project": "./tsconfig.json"
- },
- "extends": [
- "eslint:recommended",
- "plugin:@typescript-eslint/recommended",
- "next",
- "prettier"
- ],
- "rules": {
- "@typescript-eslint/no-floating-promises": "error",
- "@typescript-eslint/naming-convention": [
- "error",
- {
- "selector": "memberLike",
- "modifiers": ["public"],
- "format": ["camelCase"],
- "leadingUnderscore": "forbid"
- }
- ],
- "eqeqeq": "error",
- "no-var": "error",
- "object-shorthand": "error",
- "prefer-arrow-callback": "error",
- "prefer-destructuring": [
- "error",
- {
- "VariableDeclarator": {
- "object": true
- }
- },
- {
- "enforceForRenamedProperties": false
- }
- ],
- "no-unused-vars": "off",
- "@typescript-eslint/no-unused-vars": [
- "error",
- { "argsIgnorePattern": "^_" }
- ]
- },
- "plugins": ["react", "@typescript-eslint"]
-}
diff --git a/.gitignore b/.gitignore
index e0a4dc30..7ba0f0b3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,46 +1,109 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+vite.config.ts.*
-# dependencies
-/node_modules
-/.pnp
-.pnp.js
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
-# testing
-/coverage
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
-# next.js
-/.next/
-/out/
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
-# production
-/build
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
-# misc
-.DS_Store
-*.pem
+# nyc test coverage
+.nyc_output
-# debug
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
-# local env files
-.env.local
-.env.development.local
-.env.test.local
-.env.production.local
+# Bower dependency directory (https://bower.io/)
+bower_components
-# vercel
-.vercel
+# node-waf configuration
+.lock-wscript
-# react-designer
-lib
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
-#tsc
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# TypeScript v1 declaration files
+typings/
+
+# TypeScript cache
*.tsbuildinfo
-# Supabase
-**/supabase/.branches
-**/supabase/.temp
-**/supabase/.env
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
.env
+.env.test
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+
+# Next.js build output
+.next
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and *not* Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+
+.DS_Store
+archive.sh
\ No newline at end of file
diff --git a/.jshintrc b/.jshintrc
deleted file mode 100644
index d9fb1898..00000000
--- a/.jshintrc
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "node": true,
- "browser": true,
- "esnext": true,
- "newcap": false
-}
diff --git a/.npmrc b/.npmrc
deleted file mode 100644
index 37cfe26a..00000000
--- a/.npmrc
+++ /dev/null
@@ -1 +0,0 @@
-unsafe-perm = true
\ No newline at end of file
diff --git a/.prettierignore b/.prettierignore
deleted file mode 100644
index e3b3fe77..00000000
--- a/.prettierignore
+++ /dev/null
@@ -1,34 +0,0 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-
-# dependencies
-/node_modules
-/.pnp
-.pnp.js
-
-# testing
-/coverage
-
-# next.js
-/.next/
-/out/
-
-# production
-/build
-
-# misc
-.DS_Store
-*.pem
-
-# debug
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-
-# local env files
-.env.local
-.env.development.local
-.env.test.local
-.env.production.local
-
-# vercel
-.vercel
\ No newline at end of file
diff --git a/.prettierrc.json b/.prettierrc.json
deleted file mode 100644
index b0646c18..00000000
--- a/.prettierrc.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "singleQuote": false,
- "trailingComma": "es5",
- "arrowParens": "always",
- "bracketSpacing": true,
- "tabWidth": 2,
- "useTabs": false
-}
diff --git a/.vscode b/.vscode
deleted file mode 100644
index 57810d34..00000000
--- a/.vscode
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "editor.formatOnPaste": true,
- "editor.formatOnSave": true,
- "editor.defaultFormatter": "esbenp.prettier-vscode",
- "editor.codeActionsOnSave": {
- "source.fixAll.eslint": true,
- "source.fixAll.format": true
- }
-}
diff --git a/LICENSE b/LICENSE
index 9cf10627..163dc218 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,19 +1,13 @@
-MIT License
+Copyright 2022 Rocicorp LLC
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
+http://www.apache.org/licenses/LICENSE-2.0
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
\ No newline at end of file
diff --git a/README.md b/README.md
index 383c667f..ab7a1cfe 100644
--- a/README.md
+++ b/README.md
@@ -1,39 +1,55 @@
-# Repliear
+
-A high-performance issue tracker in the style of [Linear](https://linear.app/).
+# repliear-row-versioning
-Built with [Replicache](https://replicache.dev), [Next.js](https://nextjs.org/),
-[Pusher](https://pusher.com/), and [Postgres](https://www.postgresql.org/).
+This is a demonstration of the [Row Version Strategy](https://doc.replicache.dev/strategies/row-version).
-Running at [repliear.herokuapp.com](https://repliear.herokuapp.com/).
+A high-performance issue tracker in the style of Linear.
-# Prerequisites
+Built with [Replicache](https://replicache.dev), [ViteJS](https://vitejs.dev/),
+and [Postgres](https://www.postgresql.org/).
-1. [Get a Replicache license key](https://doc.replicache.dev/licensing)
-2. Install PostgreSQL. On MacOS, we recommend using [Postgres.app](https://postgresapp.com/). For other OSes and options, see [Postgres Downloads](https://www.postgresql.org/download/).
-3. [Sign up for a free pusher.com account](https://pusher.com/) and create a new "channels" app.
-# To run locally
+## 1. Setup
-Get the Pusher environment variables from the ["App Keys" section](https://i.imgur.com/7DNmTKZ.png) of the Pusher App UI.
+#### Get your Replicache License Key
-**Note:** These instructions assume you installed PostgreSQL via Postgres.app on MacOS. If you installed some other way, or configured PostgreSQL specially, you may additionally need to set the `PGUSER` and `PGPASSWORD` environment variables.
+```bash
+$ npx replicache get-license
+```
+
+#### Set your `VITE_REPLICACHE_LICENSE_KEY` environment variable
+
+```bash
+$ export VITE_REPLICACHE_LICENSE_KEY=""
+```
+
+#### Install Postgres
+
+Install PostgreSQL. On MacOS, we recommend using [Postgres.app](https://postgresapp.com/). For other OSes and options, see [Postgres Downloads](https://www.postgresql.org/download/).
+
+Once installed, set your database url
+```bash
+$ export DATABASE_URL="postgresql://localhost/repliear"
```
-export PGDATABASE="repliear"
-export NEXT_PUBLIC_REPLICACHE_LICENSE_KEY=""
-export NEXT_PUBLIC_PUSHER_APP_ID=
-export NEXT_PUBLIC_PUSHER_KEY=
-export NEXT_PUBLIC_PUSHER_SECRET=
-export NEXT_PUBLIC_PUSHER_CLUSTER=
-
-# Create a new database for Repliear
-psql -d postgres -c 'create database repliear'
-
-npm install
-npm run dev
+
+and create a postrgres DB
+
+```bash
+$ psql -d postgres -c 'create database repliear'
```
-## Credits
+#### Install and Build
+
+```bash
+$ npm install; npm run build;
+```
+
+## 2. Start frontend and backend watcher
+
+```bash
+$ npm run watch --ws
+```
-We started this project by forking [linear_clone](https://github.com/tuan3w/linearapp_clone). This enabled us to get the visual styling right much faster than we otherwise could have.
+Provides an example integrating replicache with react in a simple todo application.
diff --git a/backend/comments-react.d.ts b/backend/comments-react.d.ts
deleted file mode 100644
index 43fb9936..00000000
--- a/backend/comments-react.d.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-declare module "*comments-react.js.gz" {
- const gitHubComments: {
- number: number;
- // eslint-disable-next-line @typescript-eslint/naming-convention
- comment_id: string;
- body: string | null;
- // eslint-disable-next-line @typescript-eslint/naming-convention
- updated_at: string;
- // eslint-disable-next-line @typescript-eslint/naming-convention
- created_at: string;
-
- // eslint-disable-next-line @typescript-eslint/naming-convention
- creator_user_login: string;
- }[];
- export default gitHubComments;
-}
diff --git a/backend/data.test.ts b/backend/data.test.ts
deleted file mode 100644
index 6d7706cc..00000000
--- a/backend/data.test.ts
+++ /dev/null
@@ -1,390 +0,0 @@
-import { expect } from "chai";
-import { Issue, Priority, Status, Comment } from "../frontend/issue";
-import { setup, teardown, test } from "mocha";
-import type { JSONValue } from "replicache";
-import {
- createDatabase,
- delEntries,
- getEntry,
- getVersion,
- initSpace,
- putEntries,
- SampleData,
- BASE_SPACE_ID,
- getIssueEntries,
- getNonIssueEntriesInSyncOrder,
-} from "./data";
-import { transact, withExecutor } from "./pg";
-
-const i1: Issue = {
- priority: Priority.HIGH,
- id: "1",
- title: "Issue 1",
- status: Status.IN_PROGRESS,
- modified: 0,
- created: 0,
- creator: "testUser1",
- kanbanOrder: "1",
-};
-
-const comment1i1: Comment = {
- id: "1",
- issueID: "1",
- created: 0,
- body: "Comment 1",
- creator: "testUser1",
-};
-
-const comment2i1: Comment = {
- id: "2",
- issueID: "1",
- created: 0,
- body: "Comment 2",
- creator: "testUser2",
-};
-
-const i2: Issue = {
- priority: Priority.MEDIUM,
- id: "2",
- title: "Issue 2",
- status: Status.IN_PROGRESS,
- modified: 0,
- created: 0,
- creator: "testUser2",
- kanbanOrder: "2",
-};
-
-const comment1i2: Comment = {
- id: "1",
- issueID: "2",
- created: 0,
- body: "Comment 1",
- creator: "testUser1",
-};
-
-const i3: Issue = {
- priority: Priority.LOW,
- id: "3",
- title: "Issue 3",
- status: Status.TODO,
- modified: 0,
- created: 0,
- creator: "testUser3",
- kanbanOrder: "3",
-};
-
-export const testSampleData: SampleData = [
- {
- issue: i1,
- description: "Description 1",
- comments: [comment1i1, comment2i1],
- },
- { issue: i2, description: "Description 2", comments: [comment1i2] },
- { issue: i3, description: "Description 3", comments: [] },
-];
-
-function getTestSyncOrder(key: string) {
- return `${key}-testSyncOrder`;
-}
-
-setup(async () => {
- // TODO: This is a very expensive way to unit test :).
- // Is there an in-memory postgres or something?
- await transact((executor) => createDatabase(executor));
-});
-
-teardown(async () => {
- await withExecutor(async (executor) => {
- await executor(`delete from entry where spaceid = $1`, [BASE_SPACE_ID]);
- await executor(`delete from space where id = $1`, [BASE_SPACE_ID]);
- await executor(`delete from entry where spaceid like 'test-s-%'`);
- await executor(`delete from space where id like 'test-s-%'`);
- });
-});
-
-test("getEntry", async () => {
- type Case = {
- name: string;
- exists: boolean;
- deleted: boolean;
- validJSON: boolean;
- };
- const cases: Case[] = [
- {
- name: "does not exist",
- exists: false,
- deleted: false,
- validJSON: false,
- },
- {
- name: "exists, deleted",
- exists: true,
- deleted: true,
- validJSON: true,
- },
- {
- name: "exists, not deleted, invalid JSON",
- exists: true,
- deleted: false,
- validJSON: false,
- },
- {
- name: "exists, not deleted, valid JSON",
- exists: true,
- deleted: false,
- validJSON: true,
- },
- ];
-
- await withExecutor(async (executor) => {
- for (const c of cases) {
- await executor(
- `delete from entry where spaceid = 'test-s-s1' and key = 'foo'`
- );
- if (c.exists) {
- await executor(
- `insert into entry (spaceid, key, value, syncorder, deleted, version, lastmodified) values ('test-s-s1', 'foo', $1, '${getTestSyncOrder(
- "foo"
- )}',$2, 1, now())`,
- [c.validJSON ? JSON.stringify(42) : "not json", c.deleted]
- );
- }
-
- const promise = getEntry(executor, "test-s-s1", "foo");
- let result: JSONValue | undefined;
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- let error: any | undefined;
- await promise.then(
- (r) => (result = r),
- (e) => (error = String(e))
- );
- if (!c.exists) {
- expect(result, c.name).undefined;
- expect(error, c.name).undefined;
- } else if (c.deleted) {
- expect(result, c.name).undefined;
- expect(error, c.name).undefined;
- } else if (!c.validJSON) {
- expect(result, c.name).undefined;
- expect(error, c.name).contains("SyntaxError");
- } else {
- expect(result, c.name).eq(42);
- expect(error, c.name).undefined;
- }
- }
- });
-});
-
-test("getEntry RoundTrip types", async () => {
- await withExecutor(async (executor) => {
- await putEntries(
- executor,
- "test-s-s1",
- [
- ["boolean", true, getTestSyncOrder("boolean")],
- ["number", 42, getTestSyncOrder("number")],
- ["string", "foo", getTestSyncOrder("string")],
- ["array", [1, 2, 3], getTestSyncOrder("array")],
- ["object", { a: 1, b: 2 }, getTestSyncOrder("object")],
- ],
- 1
- );
- expect(await getEntry(executor, "test-s-s1", "boolean")).eq(true);
- expect(await getEntry(executor, "test-s-s1", "number")).eq(42);
- expect(await getEntry(executor, "test-s-s1", "string")).eq("foo");
- expect(await getEntry(executor, "test-s-s1", "array")).deep.equal([
- 1,
- 2,
- 3,
- ]);
- expect(await getEntry(executor, "test-s-s1", "object")).deep.equal({
- a: 1,
- b: 2,
- });
- });
-});
-
-test("putEntries", async () => {
- type Case = {
- name: string;
- duplicate: boolean;
- deleted: boolean;
- };
-
- const cases: Case[] = [
- {
- name: "no duplicate",
- duplicate: false,
- deleted: false,
- },
- {
- name: "duplicate",
- duplicate: true,
- deleted: false,
- },
- {
- name: "deleted",
- duplicate: true,
- deleted: true,
- },
- ];
-
- await withExecutor(async (executor) => {
- for (const c of cases) {
- await executor(
- `delete from entry where spaceid = 'test-s-s1' and key = 'bar'`
- );
- await executor(
- `delete from entry where spaceid = 'test-s-s1' and key = 'foo'`
- );
-
- if (c.duplicate) {
- await putEntries(
- executor,
- "test-s-s1",
- [["foo", 41, getTestSyncOrder("foo")]],
- 1
- );
- if (c.deleted) {
- await delEntries(executor, "test-s-s1", ["foo"], 1);
- }
- }
- const res: Promise = putEntries(
- executor,
- "test-s-s1",
- [
- ["bar", 100, getTestSyncOrder("bar")],
- ["foo", 42, getTestSyncOrder("foo")],
- ],
- 2
- );
- await res.catch(() => ({}));
-
- const qr = await executor(
- `select spaceid, key, value, deleted, version
- from entry where spaceid = 'test-s-s1' and key in ('bar', 'foo') order by key`
- );
- const [barRow, fooRow] = qr.rows;
-
- expect(fooRow, c.name).not.undefined;
- {
- const { spaceid, key, value, deleted, version } = fooRow;
- expect(spaceid, c.name).eq("test-s-s1");
- expect(key, c.name).eq("foo");
- expect(value, c.name).eq("42");
- expect(deleted, c.name).false;
- expect(version, c.name).eq(2);
- }
- {
- const { spaceid, key, value, deleted, version } = barRow;
- expect(spaceid, c.name).eq("test-s-s1");
- expect(key, c.name).eq("bar");
- expect(value, c.name).eq("100");
- expect(deleted, c.name).false;
- expect(version, c.name).eq(2);
- }
- }
- });
-});
-
-test("delEntries", async () => {
- type Case = {
- name: string;
- exists: boolean;
- };
- const cases: Case[] = [
- {
- name: "does not exist",
- exists: false,
- },
- {
- name: "exists",
- exists: true,
- },
- ];
- for (const c of cases) {
- await withExecutor(async (executor) => {
- await executor(
- `delete from entry where spaceid = 'test-s-s1' and key = 'bar'`
- );
- await executor(
- `delete from entry where spaceid = 'test-s-s1' and key = 'foo'`
- );
- await executor(
- `insert into entry (spaceid, key, value, syncorder, deleted, version, lastmodified) values ('test-s-s1', 'bar', '100', '${getTestSyncOrder(
- "bar"
- )}', false, 1, now())`
- );
- if (c.exists) {
- await executor(
- `insert into entry (spaceid, key, value, syncorder, deleted, version, lastmodified) values ('test-s-s1', 'foo', '42', '${getTestSyncOrder(
- "foo"
- )}',false, 1, now())`
- );
- }
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- let error: any | undefined;
- await delEntries(executor, "test-s-s1", ["bar", "foo"], 2).catch(
- (e) => (error = String(e))
- );
-
- const qr = await executor(
- `
- select spaceid, key, value, deleted, version from entry
- where spaceid = 'test-s-s1' and key in ('bar', 'foo') order by key
- `
- );
- const [barRow, fooRow] = qr.rows;
-
- expect(barRow, c.name).not.undefined;
- const { spaceid, key, value, deleted, version } = barRow;
- expect(spaceid, c.name).eq("test-s-s1");
- expect(key, c.name).eq("bar");
- expect(value, c.name).eq("100");
- expect(deleted, c.name).true;
- expect(version, c.name).eq(2);
- if (c.exists) {
- expect(fooRow, c.name).not.undefined;
- const { spaceid, key, value, deleted, version } = fooRow;
- expect(spaceid, c.name).eq("test-s-s1");
- expect(key, c.name).eq("foo");
- expect(value, c.name).eq("42");
- expect(deleted, c.name).true;
- expect(version, c.name).eq(2);
- } else {
- expect(fooRow, c.name).undefined;
- expect(error, c.name).undefined;
- }
- });
- }
-});
-
-test("initSpace", async () => {
- await withExecutor(async (executor) => {
- await executor(`delete from entry where spaceid = $1`, [BASE_SPACE_ID]);
- await executor(`delete from space where id = $1`, [BASE_SPACE_ID]);
- const testSpaceID1 = await initSpace(executor, () =>
- Promise.resolve(testSampleData)
- );
- expect(await getVersion(executor, testSpaceID1)).eq(1);
- // 3 issues
- expect((await getIssueEntries(executor, testSpaceID1)).length).eq(3);
- // 3 descriptions, and 3 comments
- expect(
- (await getNonIssueEntriesInSyncOrder(executor, testSpaceID1, "", 10))
- .entries.length
- ).eq(6);
- const testSpaceID2 = await initSpace(executor, () => {
- throw new Error("unexpected call to getSampleIssues on subsequent calls");
- });
- expect(await getVersion(executor, testSpaceID2)).eq(1);
- // 3 issues
- expect((await getIssueEntries(executor, testSpaceID2)).length).eq(3);
- // 3 descriptions, and 3 comments
- expect(
- (await getNonIssueEntriesInSyncOrder(executor, testSpaceID2, "", 10))
- .entries.length
- ).eq(6);
- });
-});
diff --git a/backend/data.ts b/backend/data.ts
deleted file mode 100644
index 0733537a..00000000
--- a/backend/data.ts
+++ /dev/null
@@ -1,457 +0,0 @@
-import type { JSONValue } from "replicache";
-import { z } from "zod";
-import type { Executor } from "./pg";
-import { ReplicacheTransaction } from "./replicache-transaction";
-import type { Issue, Comment, Description } from "../frontend/issue";
-import { mutators } from "../frontend/mutators";
-import { flatten } from "lodash";
-import { getSyncOrder } from "./sync-order";
-import { nanoid } from "nanoid";
-
-export type SampleData = {
- issue: Issue;
- description: Description;
- comments: Comment[];
-}[];
-
-export async function createDatabase(executor: Executor) {
- const schemaVersion = await getSchemaVersion(executor);
- if (schemaVersion < 0 || schemaVersion > 2) {
- throw new Error("Unexpected schema version: " + schemaVersion);
- }
-
- if (schemaVersion === 2) {
- console.log("schemaVersion is 2 - nothing to do");
- return;
- }
-
- console.log("creating schema");
- await executor("drop schema if exists public cascade");
- await executor("create schema public");
- await executor("grant all on schema public to postgres");
- await executor("grant all on schema public to public");
- await createSchema(executor);
-}
-
-async function getSchemaVersion(executor: Executor) {
- const metaExists = await executor(`select exists(
- select from pg_tables where schemaname = 'public' and tablename = 'meta')`);
- if (!metaExists.rows[0].exists) {
- return 0;
- }
-
- const qr = await executor(
- `select value from meta where key = 'schemaVersion'`
- );
- return qr.rows[0].value;
-}
-
-// nanoid's don't include $, so cannot collide with other space ids.
-export const BASE_SPACE_ID = "$base-space-id";
-
-export async function createSchema(executor: Executor) {
- await executor(`create table meta (key text primary key, value json)`);
- await executor(`insert into meta (key, value) values ('schemaVersion', '2')`);
-
- await executor(`create table space (
- id text primary key not null,
- version integer not null,
- lastmodified timestamp(6) not null
- )`);
-
- // lastpullid is null until the client has pulled for the first time.
- await executor(`create table clientgroup (
- id text primary key not null,
- lastpullid integer null
- )`);
-
- await executor(`create table client (
- id text primary key not null,
- lastmutationid integer not null,
- version integer not null,
- clientgroupid text not null,
- lastmodified timestamp(6) not null
- )`);
-
- await executor(`create table entry (
- spaceid text not null,
- key text not null,
- value text not null,
- syncorder text not null,
- deleted boolean not null,
- version integer not null,
- lastmodified timestamp(6) not null
- )`);
-
- await executor(
- `create unique index idx_entry_spaceid_key on entry (spaceid, key)`
- );
- await executor(
- `create index idx_entry_spaceid_syncorder on entry (spaceid, syncorder)`
- );
- await executor(`create index
- on entry (spaceid, deleted)
- include (key, value, deleted)
- where key like 'issue/%'`);
- await executor(`create index on entry (spaceid)`);
- await executor(`create index on entry (deleted)`);
- await executor(`create index on entry (version)`);
- await executor(`create index on client (clientgroupid, version)`);
-}
-
-const INITIAL_SPACE_VERSION = 1;
-const INITIAL_SPACE_MUTATION_ID = 0;
-export async function initSpace(
- executor: Executor,
- getSampleData: () => Promise
-): Promise {
- const {
- rows: baseSpaceRows,
- } = await executor(`select version from space where id = $1`, [
- BASE_SPACE_ID,
- ]);
-
- if (baseSpaceRows.length === 0) {
- console.log("Initializing base space", BASE_SPACE_ID);
- await insertSpace(executor, BASE_SPACE_ID, INITIAL_SPACE_VERSION);
- const start = Date.now();
- // We have to batch insertions to work around postgres command size limits
- const sampleData = await getSampleData();
- const sampleDataBatchs: SampleData[] = [];
- for (let i = 0; i < sampleData.length; i++) {
- if (i % 1000 === 0) {
- sampleDataBatchs.push([]);
- }
- sampleDataBatchs[sampleDataBatchs.length - 1].push(sampleData[i]);
- }
- for (const sampleDataBatch of sampleDataBatchs) {
- const tx = new ReplicacheTransaction(
- executor,
- BASE_SPACE_ID,
- "fake-client-id-for-server-init",
- INITIAL_SPACE_MUTATION_ID,
- INITIAL_SPACE_VERSION,
- getSyncOrder
- );
- for (const { issue, description, comments } of sampleDataBatch) {
- await mutators.putIssue(tx, { issue, description });
- for (const comment of comments) {
- await mutators.putIssueComment(tx, comment, false);
- }
- }
- await tx.flush();
- }
- console.log("Initing base space took " + (Date.now() - start) + "ms");
- }
- const spaceID = nanoid(10);
- await insertSpace(executor, spaceID, INITIAL_SPACE_VERSION);
- return spaceID;
-}
-
-async function insertSpace(
- executor: Executor,
- spaceID: string,
- version: number
-) {
- await executor(
- `insert into space (id, version, lastmodified) values ($1, $2, now())`,
- [spaceID, version]
- );
-}
-
-export async function getEntry(
- executor: Executor,
- spaceID: string,
- key: string
-): Promise {
- const { rows } = await executor(
- `
- with overlayentry as (
- select key, value, deleted from entry where spaceid = $1 and key = $3
- ), baseentry as (
- select key, value from entry where spaceid = $2 and key = $3
- )
- select coalesce(overlayentry.key, baseentry.key),
- coalesce(overlayentry.value, baseentry.value) as value,
- overlayentry.deleted as deleted
- from overlayentry full join baseentry on overlayentry.key = baseentry.key
- `,
- [spaceID, BASE_SPACE_ID, key]
- );
- const value = rows[0]?.value;
- if (value === undefined || rows[0]?.deleted) {
- return undefined;
- }
- return JSON.parse(value);
-}
-
-export async function putEntries(
- executor: Executor,
- spaceID: string,
- entries: [key: string, value: JSONValue, syncOrder: string][],
- version: number
-): Promise {
- if (entries.length === 0) {
- return;
- }
- const valuesSql = Array.from(
- { length: entries.length },
- (_, i) =>
- `($1, $${i * 3 + 3}, $${i * 3 + 4}, $${i * 3 + 5}, false, $2, now())`
- ).join();
-
- await executor(
- `
- insert into entry (
- spaceid, key, value, syncOrder, deleted, version, lastmodified
- ) values ${valuesSql}
- on conflict (spaceid, key) do
- update set value = excluded.value, syncorder = excluded.syncorder,
- deleted = false, version = excluded.version, lastmodified = now()
- `,
- [
- spaceID,
- version,
- ...flatten(
- entries.map(([key, value, syncOrder]) => [
- key,
- JSON.stringify(value),
- syncOrder,
- ])
- ),
- ]
- );
-}
-
-export async function delEntries(
- executor: Executor,
- spaceID: string,
- keys: string[],
- version: number
-): Promise {
- if (keys.length === 0) {
- return;
- }
- const keyParamsSQL = keys.map((_, i) => `$${i + 3}`).join(",");
- await executor(
- `
- update entry set deleted = true, version = $2
- where spaceid = $1 and key in(${keyParamsSQL})
- `,
- [spaceID, version, ...keys]
- );
-}
-
-export async function getIssueEntries(
- executor: Executor,
- spaceID: string
-): Promise<[key: string, value: string][]> {
- const { rows } = await executor(
- `
- with overlayentry as (
- select key, value, deleted from entry
- where spaceid = $1 and key like 'issue/%'
- ), baseentry as (
- select key, value from entry where spaceid = $2 and key like 'issue/%'
- )
- select coalesce(overlayentry.key, baseentry.key) as key,
- coalesce(overlayentry.value, baseentry.value) as value,
- overlayentry.deleted as deleted
- from overlayentry full join baseentry on overlayentry.key = baseentry.key
- `,
- [spaceID, BASE_SPACE_ID]
- );
- const startFilter = Date.now();
- const filtered: [key: string, value: string][] = rows
- .filter((row) => !row.deleted)
- .map((row) => [row.key, row.value]);
-
- console.log("getIssueEntries filter took " + (Date.now() - startFilter));
- return filtered;
-}
-
-export async function getNonIssueEntriesInSyncOrder(
- executor: Executor,
- spaceID: string,
- startSyncOrderExclusive: string,
- limit: number
-): Promise<{
- entries: [key: string, value: string][];
- endSyncOrder: string | undefined;
-}> {
- // All though it complicates the query, we do the deleted filtering
- // in the query so that we can correctly limit the results.
- const { rows } = await executor(
- `
- with overlayentry as (
- select key, value, syncorder, deleted from entry
- where spaceid = $1 and key not like 'issue/%' and syncorder > $3
- order by syncorder limit $4
- ), baseentry as (
- select key, value, syncorder from entry
- where spaceid = $2 and key not like 'issue/%' and syncorder > $3
- order by syncorder limit $4
- )
- select key, value, syncorder from (
- select coalesce(overlayentry.key, baseentry.key) as key,
- coalesce(overlayentry.value, baseentry.value) as value,
- coalesce(overlayentry.syncorder, baseentry.syncorder) as syncorder,
- overlayentry.deleted as deleted
- from overlayentry full join baseentry on overlayentry.key = baseentry.key
- ) as merged where deleted = false or deleted is null
- order by syncorder
- limit $4
- `,
- [spaceID, BASE_SPACE_ID, startSyncOrderExclusive, limit]
- );
- return {
- entries: rows.map((row) => [row.key, row.value]),
- endSyncOrder: rows[rows.length - 1]?.syncorder,
- };
-}
-
-export async function getChangedEntries(
- executor: Executor,
- spaceID: string,
- prevVersion: number
-): Promise<[key: string, value: string, deleted: boolean][]> {
- // changes are only in the onverlay space, so we do not need to
- // query the base space.
- const {
- rows,
- } = await executor(
- `select key, value, deleted from entry where spaceid = $1 and version > $2`,
- [spaceID, prevVersion]
- );
- return rows.map((row) => [row.key, row.value, row.deleted]);
-}
-
-export async function getVersion(
- executor: Executor,
- spaceID: string
-): Promise {
- const { rows } = await executor(`select version from space where id = $1`, [
- spaceID,
- ]);
- const value = rows[0]?.version;
- if (value === undefined) {
- return undefined;
- }
- return z.number().parse(value);
-}
-
-export async function setVersion(
- executor: Executor,
- spaceID: string,
- version: number
-): Promise {
- await executor(
- `update space set version = $2, lastmodified = now() where id = $1`,
- [spaceID, version]
- );
-}
-
-export async function getLastMutationID(
- executor: Executor,
- clientID: string
-): Promise {
- const {
- rows,
- } = await executor(`select lastmutationid from client where id = $1`, [
- clientID,
- ]);
- const value = rows[0]?.lastmutationid;
- if (value === undefined) {
- return undefined;
- }
- return z.number().parse(value);
-}
-
-export async function getLastMutationIDs(
- executor: Executor,
- clientIDs: string[]
-) {
- return Object.fromEntries(
- await Promise.all(
- clientIDs.map(async (cid) => {
- const lmid = await getLastMutationID(executor, cid);
- return [cid, lmid ?? 0] as const;
- })
- )
- );
-}
-
-export async function getLastMutationIDsSince(
- executor: Executor,
- clientGroupID: string,
- sinceVersion: number
-) {
- const {
- rows,
- } = await executor(
- `select id, clientgroupid, lastmutationid from client where clientgroupid = $1 and version > $2`,
- [clientGroupID, sinceVersion]
- );
- return Object.fromEntries(
- rows.map((r) => [r.id as string, r.lastmutationid as number] as const)
- );
-}
-
-export async function incrementPullID(
- executor: Executor,
- clientGroupID: string
-) {
- const {
- rows,
- } = await executor(`select lastpullid from clientgroup where id = $1`, [
- clientGroupID,
- ]);
- if (rows.length === 0) {
- await executor(`insert into clientgroup (id, lastpullid) values ($1, 1)`, [
- clientGroupID,
- ]);
- return 1;
- }
- const [prev] = rows;
- const { lastpullid } = prev;
- const nextPullID = lastpullid + 1;
- await executor(`update clientgroup set lastpullid = $1`, [nextPullID]);
- return nextPullID;
-}
-
-export async function setLastMutationID(
- executor: Executor,
- clientID: string,
- clientGroupID: string,
- lastMutationID: number,
- version: number
-): Promise {
- await executor(
- `
- insert into clientgroup (id, lastpullid) values ($1, null)
- on conflict (id) do nothing
- `,
- [clientGroupID]
- );
- await executor(
- `
- insert into client (id, clientgroupid, lastmutationid, version, lastmodified)
- values ($1, $2, $3, $4, now())
- on conflict (id) do update set lastmutationid = $3, version = $4, lastmodified = now()
- `,
- [clientID, clientGroupID, lastMutationID, version]
- );
-}
-
-export async function setLastMutationIDs(
- executor: Executor,
- clientGroupID: string,
- lmids: Record,
- version: number
-) {
- return await Promise.all(
- [...Object.entries(lmids)].map(([clientID, lmid]) =>
- setLastMutationID(executor, clientID, clientGroupID, lmid, version)
- )
- );
-}
diff --git a/backend/issues-react.d.ts b/backend/issues-react.d.ts
deleted file mode 100644
index 08853228..00000000
--- a/backend/issues-react.d.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-declare module "*issues-react.js.gz" {
- const gitHubIssues: {
- number: number;
- title: string;
- body: string | null;
- state: "open" | "closed";
- // eslint-disable-next-line @typescript-eslint/naming-convention
- updated_at: string;
- // eslint-disable-next-line @typescript-eslint/naming-convention
- created_at: string;
- // eslint-disable-next-line @typescript-eslint/naming-convention
- creator_user_login: string;
- }[];
- export default gitHubIssues;
-}
diff --git a/backend/issues-react.js.gz b/backend/issues-react.js.gz
deleted file mode 100644
index 190aff5518b44d348d84802e58348e38a9c7ed74..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 4423605
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