diff --git a/TODO.md b/TODO.md index 965858c..3b7b4f3 100644 --- a/TODO.md +++ b/TODO.md @@ -16,5 +16,3 @@ status stepper queue up wantlist adds to gracefully handle rate limiting - - diff --git a/api/index.ts b/api/index.ts index f8a6823..36f2a22 100644 --- a/api/index.ts +++ b/api/index.ts @@ -26,10 +26,10 @@ async function start() { discogs, api, events, + listenNotify, } = createServices(log); - // events.emit(Events.Sync); - // events.emit(Events.ListingsUpdated); + await listenNotify.connect(); api.listen(); } diff --git a/api/package-lock.json b/api/package-lock.json index 0564ae8..e6968fd 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -23,7 +23,8 @@ "koa-websocket": "^7.0.0", "koa-ws": "^1.0.1", "lodash": "^4.17.21", - "passport-local": "^1.0.0" + "passport-local": "^1.0.0", + "pg-listen": "^1.7.0" }, "devDependencies": { "@types/bcrypt": "^5.0.0", @@ -697,6 +698,15 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "peer": true, + "engines": { + "node": ">=4" + } + }, "node_modules/bufferutil": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-1.1.0.tgz", @@ -2892,6 +2902,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==", + "peer": true + }, "node_modules/parse5": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.0.0.tgz", @@ -2991,6 +3007,129 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, + "node_modules/pg": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.8.0.tgz", + "integrity": "sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==", + "peer": true, + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.5.0", + "pg-pool": "^3.5.2", + "pg-protocol": "^1.5.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-connection-string": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", + "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==", + "peer": true + }, + "node_modules/pg-format": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pg-format/-/pg-format-1.0.4.tgz", + "integrity": "sha512-YyKEF78pEA6wwTAqOUaHIN/rWpfzzIuMh9KdAhc3rSLQ/7zkRFcCgYBAEGatDstLyZw4g0s9SNICmaTGnBVeyw==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "peer": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-listen": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/pg-listen/-/pg-listen-1.7.0.tgz", + "integrity": "sha512-MKDwKLm4ryhy7iq1yw1K1MvUzBdTkaT16HZToddX9QaT8XSdt3Kins5mYH6DLECGFzFWG09VdXvWOIYogjXrsg==", + "dependencies": { + "debug": "^4.1.1", + "pg-format": "^1.0.4", + "typed-emitter": "^0.1.0" + }, + "peerDependencies": { + "pg": "7.x || 8.x" + } + }, + "node_modules/pg-listen/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/pg-listen/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/pg-pool": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.2.tgz", + "integrity": "sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==", + "peer": true, + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz", + "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==", + "peer": true + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "peer": true, + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "peer": true, + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -3003,6 +3142,45 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "peer": true, + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prisma": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.5.0.tgz", @@ -3234,6 +3412,15 @@ "source-map": "^0.6.0" } }, + "node_modules/split2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", + "integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==", + "peer": true, + "engines": { + "node": ">= 10.x" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -3483,6 +3670,11 @@ "node": ">= 0.6" } }, + "node_modules/typed-emitter": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-0.1.0.tgz", + "integrity": "sha512-Tfay0l6gJMP5rkil8CzGbLthukn+9BN/VXWcABVFPjOoelJ+koW8BuPZYk+h/L+lEeIp1fSzVRiWRPIjKVjPdg==" + }, "node_modules/typescript": { "version": "4.7.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", @@ -3636,7 +3828,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, "engines": { "node": ">=0.4" } @@ -4276,6 +4467,12 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "peer": true + }, "bufferutil": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-1.1.0.tgz", @@ -5891,6 +6088,12 @@ "p-limit": "^3.0.2" } }, + "packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==", + "peer": true + }, "parse5": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.0.0.tgz", @@ -5962,12 +6165,131 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, + "pg": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.8.0.tgz", + "integrity": "sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==", + "peer": true, + "requires": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.5.0", + "pg-pool": "^3.5.2", + "pg-protocol": "^1.5.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + } + }, + "pg-connection-string": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", + "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==", + "peer": true + }, + "pg-format": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pg-format/-/pg-format-1.0.4.tgz", + "integrity": "sha512-YyKEF78pEA6wwTAqOUaHIN/rWpfzzIuMh9KdAhc3rSLQ/7zkRFcCgYBAEGatDstLyZw4g0s9SNICmaTGnBVeyw==" + }, + "pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "peer": true + }, + "pg-listen": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/pg-listen/-/pg-listen-1.7.0.tgz", + "integrity": "sha512-MKDwKLm4ryhy7iq1yw1K1MvUzBdTkaT16HZToddX9QaT8XSdt3Kins5mYH6DLECGFzFWG09VdXvWOIYogjXrsg==", + "requires": { + "debug": "^4.1.1", + "pg-format": "^1.0.4", + "typed-emitter": "^0.1.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "pg-pool": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.2.tgz", + "integrity": "sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==", + "peer": true, + "requires": {} + }, + "pg-protocol": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz", + "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==", + "peer": true + }, + "pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "peer": true, + "requires": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + } + }, + "pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "peer": true, + "requires": { + "split2": "^4.1.0" + } + }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, + "postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "peer": true + }, + "postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "peer": true + }, + "postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "peer": true + }, + "postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "peer": true, + "requires": { + "xtend": "^4.0.0" + } + }, "prisma": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.5.0.tgz", @@ -6138,6 +6460,12 @@ "source-map": "^0.6.0" } }, + "split2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", + "integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==", + "peer": true + }, "statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -6304,6 +6632,11 @@ "mime-types": "~2.1.24" } }, + "typed-emitter": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-0.1.0.tgz", + "integrity": "sha512-Tfay0l6gJMP5rkil8CzGbLthukn+9BN/VXWcABVFPjOoelJ+koW8BuPZYk+h/L+lEeIp1fSzVRiWRPIjKVjPdg==" + }, "typescript": { "version": "4.7.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", @@ -6427,8 +6760,7 @@ "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, "y18n": { "version": "5.0.8", diff --git a/api/package.json b/api/package.json index 8030a83..404b712 100644 --- a/api/package.json +++ b/api/package.json @@ -41,7 +41,8 @@ "koa-websocket": "^7.0.0", "koa-ws": "^1.0.1", "lodash": "^4.17.21", - "passport-local": "^1.0.0" + "passport-local": "^1.0.0", + "pg-listen": "^1.7.0" }, "overrides": { "koa-passport": { diff --git a/api/prisma/migrations/20221226110634_initial_migration/migration.sql b/api/prisma/migrations/20221226110634_initial_migration/migration.sql new file mode 100644 index 0000000..ea38377 --- /dev/null +++ b/api/prisma/migrations/20221226110634_initial_migration/migration.sql @@ -0,0 +1,81 @@ +-- CreateTable +CREATE TABLE "listings" ( + "id" BIGINT NOT NULL, + "seller" BIGINT NOT NULL, + "data" JSONB NOT NULL, + "wantId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "listings_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "users" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "users_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "wants" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "release" BIGINT NOT NULL, + "data" JSONB NOT NULL, + "userId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "wants_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "offers" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "status" TEXT NOT NULL, + "condition" TEXT NOT NULL DEFAULT '', + "item" MONEY NOT NULL, + "shipping" MONEY NOT NULL, + "total" MONEY NOT NULL, + "listingId" BIGINT NOT NULL, + "userId" UUID NOT NULL, + "wantId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "offers_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "listings_seller_idx" ON "listings"("seller"); + +-- CreateIndex +CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); + +-- CreateIndex +CREATE INDEX "wants_release_idx" ON "wants"("release"); + +-- CreateIndex +CREATE UNIQUE INDEX "wants_userId_release_key" ON "wants"("userId", "release"); + +-- CreateIndex +CREATE UNIQUE INDEX "offers_listingId_key" ON "offers"("listingId"); + +-- AddForeignKey +ALTER TABLE "listings" ADD CONSTRAINT "listings_wantId_fkey" FOREIGN KEY ("wantId") REFERENCES "wants"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "wants" ADD CONSTRAINT "wants_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "offers" ADD CONSTRAINT "offers_listingId_fkey" FOREIGN KEY ("listingId") REFERENCES "listings"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "offers" ADD CONSTRAINT "offers_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "offers" ADD CONSTRAINT "offers_wantId_fkey" FOREIGN KEY ("wantId") REFERENCES "wants"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/api/prisma/migrations/20221226113955_notify/migration.sql b/api/prisma/migrations/20221226113955_notify/migration.sql new file mode 100644 index 0000000..dd672dc --- /dev/null +++ b/api/prisma/migrations/20221226113955_notify/migration.sql @@ -0,0 +1,65 @@ +CREATE OR REPLACE FUNCTION notify_uuid() RETURNS TRIGGER AS $$ + DECLARE + record RECORD; + id UUID; + BEGIN + IF (TG_OP = 'DELETE') THEN + id = OLD.id; + ELSE + id = NEW.id; + END IF; + + payload = json_build_object( + 'table', TG_TABLE_NAME, + 'action', TG_OP, + 'id', id + ); + + PERFORM pg_notify('events', payload::text); + + RETURN NULL; + END; + +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION notify_bigint() RETURNS TRIGGER AS $$ + DECLARE + record RECORD; + id int8; + BEGIN + IF (TG_OP = 'DELETE') THEN + id = OLD.id; + ELSE + id = NEW.id; + END IF; + + payload = json_build_object( + 'table', TG_TABLE_NAME, + 'action', TG_OP, + 'id', id + ); + + PERFORM pg_notify('events', payload::text); + + RETURN NULL; + END; + +$$ LANGUAGE plpgsql; + + +CREATE TRIGGER notify_listings_event +AFTER INSERT OR UPDATE OR DELETE ON listings + FOR EACH ROW EXECUTE PROCEDURE notify_bigint(); + +CREATE TRIGGER notify_wants_event +AFTER INSERT OR UPDATE OR DELETE ON wants + FOR EACH ROW EXECUTE PROCEDURE notify_bigint(); + +CREATE TRIGGER notify_offers_event +AFTER INSERT OR UPDATE OR DELETE ON offers + FOR EACH ROW EXECUTE PROCEDURE notify_uuid(); + +CREATE TRIGGER notify_users_event +AFTER INSERT OR UPDATE OR DELETE ON users + FOR EACH ROW EXECUTE PROCEDURE notify_uuid(); + diff --git a/api/prisma/migrations/migration_lock.toml b/api/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/api/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/api/src/api.ts b/api/src/api.ts index df45f0d..77fbc40 100644 --- a/api/src/api.ts +++ b/api/src/api.ts @@ -109,7 +109,6 @@ export default function createApi(log: Log, pg: Pg, discogs: Discogs, offers: Of const ws = ctx.websocket; const rpc = createRpc(log, pg, ws, discogs, offers, events, ctx.state.user as User | null); - if (ctx.state.user) events.emit(Events.Connection, { ws, id: ctx.state.user.id }); ws.on('message', async function incoming(message) { try { const parsed = JSON.parse(message.toString()); @@ -127,7 +126,9 @@ export default function createApi(log: Log, pg: Pg, discogs: Discogs, offers: Of log.info('ws disconnected', { user: ctx.state.user }); }); + if (ctx.state.user) events.emit(Events.Connection, { rpc, id: ctx.state.user.id }); await rpc.onConnection(); + } catch (e) { log.error(e); } diff --git a/api/src/events.ts b/api/src/events.ts index ba6ab62..534656f 100644 --- a/api/src/events.ts +++ b/api/src/events.ts @@ -2,9 +2,10 @@ import { EventEmitter } from 'node:events'; import { setTimeout } from 'timers/promises'; import type Log from 'bunyan'; -import type { Want } from '@prisma/client'; +import type { Want, Offer } from '@prisma/client'; import { Pg } from './pg'; +import { Rpc } from './rpc'; import { wantDetails } from './utils'; import routerAuth from './routers/router.auth'; @@ -13,11 +14,13 @@ export enum Events { Sync = 'Sync', ListingsUpdated = 'ListingsUpdated', Connection = 'Connection', + + OffersUpdated = 'OffersUpdated' } export default function createEvents(log: Log, pg: Pg, discogs, offers) { const events = new EventEmitter({ captureRejections: true }); - const connections = new Map(); + const connections = new Map(); async function populateListings(want: Want) { try { @@ -66,9 +69,17 @@ export default function createEvents(log: Log, pg: Pg, discogs, offers) { log.debug({ offers: updatedOffers.length }, 'offers up to date'); }); - events.on(Events.Connection, async function onConnection({ ws, id }) { - connections.set(id, ws); + events.on(Events.Connection, async function onConnection({ rpc, id }) { + log.debug('new rpc connection', { id, rpc }); + connections.set(id, rpc); }) + events.on(Events.OffersUpdated, async function onOffersUpdated(offer: Offer) { + const rpc = connections.get(offer.userId); + if (!rpc) return null; + log.debug({ event: Events.OffersUpdated, rpc, offer }); + return rpc.sendUserOffers(); + }); + return events; } \ No newline at end of file diff --git a/api/src/listen.notify.ts b/api/src/listen.notify.ts new file mode 100644 index 0000000..5bb3ace --- /dev/null +++ b/api/src/listen.notify.ts @@ -0,0 +1,20 @@ +import createSubscriber from 'pg-listen' +import type Log from 'bunyan'; + +import { Events } from './events'; + +export default function createListenNotify(log: Log, events) { + const CHANNEL = 'events'; + const subscriber = createSubscriber({ connectionString: process.env.DATABASE_URL }); + + subscriber.notifications.on(CHANNEL, ({ table, action, record }) => { + if (table === 'offers') return events.emit(Events.OffersUpdated, record); + }) + + async function connect() { + await subscriber.connect(); + return subscriber.listenTo(CHANNEL); + } + + return { connect }; +} \ No newline at end of file diff --git a/api/src/rpc.ts b/api/src/rpc.ts index 3b367d4..7456ceb 100644 --- a/api/src/rpc.ts +++ b/api/src/rpc.ts @@ -134,9 +134,7 @@ export default function createRpc(log: Log, pg: Pg, ws, discogs: Discogs, offers const userMsg: UserMessage = [RpcMessageType.User, user]; send(userMsg); - const userOffers = await offers.userOffers(user); - const offersMsg: OffersMessage = [RpcMessageType.Offers, userOffers]; - send(offersMsg); + await sendUserOffers(); const wants = await pg.userWants(user); const wantsMsg: WantsMessage = [RpcMessageType.Wants, wants]; @@ -153,10 +151,18 @@ export default function createRpc(log: Log, pg: Pg, ws, discogs: Discogs, offers )) } + async function sendUserOffers() { + const userOffers = await offers.userOffers(user); + const offersMsg: OffersMessage = [RpcMessageType.Offers, userOffers]; + send(offersMsg); + } + return { receive, send, onConnection, + + sendUserOffers, } } diff --git a/api/src/services.ts b/api/src/services.ts index 0012178..8f0fb2d 100644 --- a/api/src/services.ts +++ b/api/src/services.ts @@ -6,19 +6,22 @@ import createLog from './log'; import createApi from './api'; import createEvents from './events'; import createPg from './pg'; +import createListenNotify from './listen.notify'; -export default function creates(log) { +export default function createServices(log) { const prisma = new PrismaClient(); const pg = createPg(log, prisma); const discogs = createDiscogs(log, pg); const offers = createOffers(log, pg); const events = createEvents(log, pg, discogs, offers); + const listenNotify = createListenNotify(log, events); const api = createApi(log, pg, discogs, offers, events); return { api, discogs, events, + listenNotify, pg, prisma, };