From dbf19eeb60ba2b6b628d47dcecec9ca070bf3380 Mon Sep 17 00:00:00 2001 From: Sean Sube Date: Tue, 17 Jun 2025 20:18:04 -0500 Subject: [PATCH] add yaml import and export --- client/src/graphql/client.ts | 3 +- package-lock.json | 397 +++++++++++++++++++--- package.json | 6 + server/package.json | 2 + server/src/__tests__/yaml-service.test.ts | 75 ++++ server/src/index.ts | 42 ++- server/src/printer/format-utils.ts | 4 +- server/src/printer/serial-printer.ts | 4 +- server/src/yaml-service.ts | 187 ++++++++++ 9 files changed, 665 insertions(+), 55 deletions(-) create mode 100644 server/src/__tests__/yaml-service.test.ts create mode 100644 server/src/yaml-service.ts diff --git a/client/src/graphql/client.ts b/client/src/graphql/client.ts index 72a20d5..891836c 100644 --- a/client/src/graphql/client.ts +++ b/client/src/graphql/client.ts @@ -2,7 +2,8 @@ import { ApolloClient, InMemoryCache, createHttpLink, from } from '@apollo/clien import { onError } from '@apollo/client/link/error'; const httpLink = createHttpLink({ - uri: 'http://localhost:4000/graphql', + uri: 'http://10.2.2.68:4000/graphql', + //uri: 'http://localhost:4000/graphql', }); const errorLink = onError(({ graphQLErrors, networkError, operation }) => { diff --git a/package-lock.json b/package-lock.json index b928799..6c325fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,13 @@ "server", "client", "shared" - ] + ], + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@mui/icons-material": "^7.1.1", + "@mui/material": "^7.1.1" + } }, "client": { "version": "0.0.0", @@ -17,11 +23,12 @@ "@apollo/client": "^3.9.5", "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", - "@mui/material": "^5.15.10", + "@mui/material": "^5.17.1", "@task-receipts/shared": "file:../shared", "graphql": "^16.8.1", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "zustand": "^5.0.5" }, "devDependencies": { "@types/react": "^18.2.55", @@ -36,6 +43,194 @@ "vite": "^5.1.0" } }, + "client/node_modules/@mui/material": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.17.1.tgz", + "integrity": "sha512-2B33kQf+GmPnrvXXweWAx+crbiUEsxCdCN979QDYnlH9ox4pd+0/IBriWLV+l6ORoBF60w39cWjFnJYGFdzXcw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/core-downloads-tracker": "^5.17.1", + "@mui/system": "^5.17.1", + "@mui/types": "~7.2.15", + "@mui/utils": "^5.17.1", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^19.0.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "client/node_modules/@mui/material/node_modules/@mui/system": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.17.1.tgz", + "integrity": "sha512-aJrmGfQpyF0U4D4xYwA6ueVtQcEMebET43CUmKMP7e7iFh3sMIF3sBR0l8Urb4pqx1CBjHAaWgB0ojpND4Q3Jg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/private-theming": "^5.17.1", + "@mui/styled-engine": "^5.16.14", + "@mui/types": "~7.2.15", + "@mui/utils": "^5.17.1", + "clsx": "^2.1.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "client/node_modules/@mui/material/node_modules/@mui/system/node_modules/@mui/private-theming": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.17.1.tgz", + "integrity": "sha512-XMxU0NTYcKqdsG8LRmSoxERPXwMbp16sIXPcLVgLGII/bVNagX0xaheWAwFv8+zDK7tI3ajllkuD3GZZE++ICQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/utils": "^5.17.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "client/node_modules/@mui/material/node_modules/@mui/system/node_modules/@mui/styled-engine": { + "version": "5.16.14", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.14.tgz", + "integrity": "sha512-UAiMPZABZ7p8mUW4akDV6O7N3+4DatStpXMZwPlt+H/dA0lt67qawN021MNND+4QTpjaiMYxbhKZeQcyWCbuKw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@emotion/cache": "^11.13.5", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "client/node_modules/@mui/material/node_modules/@mui/types": { + "version": "7.2.24", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz", + "integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "client/node_modules/@mui/material/node_modules/@mui/utils": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.17.1.tgz", + "integrity": "sha512-jEZ8FTqInt2WzxDV8bhImWBqeQRD99c/id/fq83H0ER9tFl+sfZlaAoCdznGvbSQQ9ividMxqSV2c7cC1vBcQg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/types": "~7.2.15", + "@types/prop-types": "^15.7.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "client/node_modules/@typescript-eslint/eslint-plugin": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", @@ -290,6 +485,12 @@ "dev": true, "license": "MIT" }, + "client/node_modules/react-is": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", + "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==", + "license": "MIT" + }, "client/node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -2491,27 +2692,53 @@ "url": "https://opencollective.com/mui-org" } }, - "node_modules/@mui/material": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.17.1.tgz", - "integrity": "sha512-2B33kQf+GmPnrvXXweWAx+crbiUEsxCdCN979QDYnlH9ox4pd+0/IBriWLV+l6ORoBF60w39cWjFnJYGFdzXcw==", + "node_modules/@mui/icons-material": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.1.1.tgz", + "integrity": "sha512-X37+Yc8QpEnl0sYmz+WcLFy2dWgNRzbswDzLPXG7QU1XDVlP5TPp1HXjdmCupOWLL/I9m1fyhcyZl8/HPpp/Cg==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/core-downloads-tracker": "^5.17.1", - "@mui/system": "^5.17.1", - "@mui/types": "~7.2.15", - "@mui/utils": "^5.17.1", + "@babel/runtime": "^7.27.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^7.1.1", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.1.1.tgz", + "integrity": "sha512-mTpdmdZCaHCGOH3SrYM41+XKvNL0iQfM9KlYgpSjgadXx/fEKhhvOktxm8++Xw6FFeOHoOiV+lzOI8X1rsv71A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.1", + "@mui/core-downloads-tracker": "^7.1.1", + "@mui/system": "^7.1.1", + "@mui/types": "^7.4.3", + "@mui/utils": "^7.1.1", "@popperjs/core": "^2.11.8", - "@types/react-transition-group": "^4.4.10", - "clsx": "^2.1.0", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1", - "react-is": "^19.0.0", + "react-is": "^19.1.0", "react-transition-group": "^4.4.5" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" }, "funding": { "type": "opencollective", @@ -2520,6 +2747,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^7.1.1", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -2531,11 +2759,24 @@ "@emotion/styled": { "optional": true }, + "@mui/material-pigment-css": { + "optional": true + }, "@types/react": { "optional": true } } }, + "node_modules/@mui/material/node_modules/@mui/core-downloads-tracker": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.1.1.tgz", + "integrity": "sha512-yBckQs4aQ8mqukLnPC6ivIRv6guhaXi8snVl00VtyojBbm+l6VbVhyTSZ68Abcx7Ah8B+GZhrB7BOli+e+9LkQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, "node_modules/@mui/material/node_modules/react-is": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", @@ -2543,17 +2784,17 @@ "license": "MIT" }, "node_modules/@mui/private-theming": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.17.1.tgz", - "integrity": "sha512-XMxU0NTYcKqdsG8LRmSoxERPXwMbp16sIXPcLVgLGII/bVNagX0xaheWAwFv8+zDK7tI3ajllkuD3GZZE++ICQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.1.tgz", + "integrity": "sha512-M8NbLUx+armk2ZuaxBkkMk11ultnWmrPlN0Xe3jUEaBChg/mcxa5HWIWS1EE4DF36WRACaAHVAvyekWlDQf0PQ==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/utils": "^5.17.1", + "@babel/runtime": "^7.27.1", + "@mui/utils": "^7.1.1", "prop-types": "^15.8.1" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" }, "funding": { "type": "opencollective", @@ -2570,18 +2811,20 @@ } }, "node_modules/@mui/styled-engine": { - "version": "5.16.14", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.14.tgz", - "integrity": "sha512-UAiMPZABZ7p8mUW4akDV6O7N3+4DatStpXMZwPlt+H/dA0lt67qawN021MNND+4QTpjaiMYxbhKZeQcyWCbuKw==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.1.tgz", + "integrity": "sha512-R2wpzmSN127j26HrCPYVQ53vvMcT5DaKLoWkrfwUYq3cYytL6TQrCH8JBH3z79B6g4nMZZVoaXrxO757AlShaw==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.23.9", + "@babel/runtime": "^7.27.1", "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", "csstype": "^3.1.3", "prop-types": "^15.8.1" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" }, "funding": { "type": "opencollective", @@ -2602,22 +2845,22 @@ } }, "node_modules/@mui/system": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.17.1.tgz", - "integrity": "sha512-aJrmGfQpyF0U4D4xYwA6ueVtQcEMebET43CUmKMP7e7iFh3sMIF3sBR0l8Urb4pqx1CBjHAaWgB0ojpND4Q3Jg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.1.1.tgz", + "integrity": "sha512-Kj1uhiqnj4Zo7PDjAOghtXJtNABunWvhcRU0O7RQJ7WOxeynoH6wXPcilphV8QTFtkKaip8EiNJRiCD+B3eROA==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/private-theming": "^5.17.1", - "@mui/styled-engine": "^5.16.14", - "@mui/types": "~7.2.15", - "@mui/utils": "^5.17.1", - "clsx": "^2.1.0", + "@babel/runtime": "^7.27.1", + "@mui/private-theming": "^7.1.1", + "@mui/styled-engine": "^7.1.1", + "@mui/types": "^7.4.3", + "@mui/utils": "^7.1.1", + "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" }, "funding": { "type": "opencollective", @@ -2642,10 +2885,13 @@ } }, "node_modules/@mui/types": { - "version": "7.2.24", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz", - "integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==", + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.3.tgz", + "integrity": "sha512-2UCEiK29vtiZTeLdS2d4GndBKacVyxGvReznGXGr+CzW/YhjIX+OHUdCIczZjzcRAgKBGmE9zCIgoV9FleuyRQ==", "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.1" + }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -2656,20 +2902,20 @@ } }, "node_modules/@mui/utils": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.17.1.tgz", - "integrity": "sha512-jEZ8FTqInt2WzxDV8bhImWBqeQRD99c/id/fq83H0ER9tFl+sfZlaAoCdznGvbSQQ9ividMxqSV2c7cC1vBcQg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.1.tgz", + "integrity": "sha512-BkOt2q7MBYl7pweY2JWwfrlahhp+uGLR8S+EhiyRaofeRYUWL2YKbSGQvN4hgSN1i8poN0PaUiii1kEMrchvzg==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/types": "~7.2.15", - "@types/prop-types": "^15.7.12", + "@babel/runtime": "^7.27.1", + "@mui/types": "^7.4.3", + "@types/prop-types": "^15.7.14", "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-is": "^19.0.0" + "react-is": "^19.1.0" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" }, "funding": { "type": "opencollective", @@ -3411,6 +3657,12 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "license": "MIT" + }, "node_modules/@types/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", @@ -11795,6 +12047,35 @@ "zen-observable": "0.8.15" } }, + "node_modules/zustand": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.5.tgz", + "integrity": "sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, "server": { "name": "task-receipts-server", "version": "1.0.0", @@ -11803,11 +12084,13 @@ "@node-escpos/core": "^0.6.0", "@node-escpos/usb-adapter": "^0.3.1", "@task-receipts/shared": "file:../shared", + "@types/js-yaml": "^4.0.9", "apollo-server-express": "^3.13.0", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.18.3", "graphql": "^16.8.1", + "js-yaml": "^4.1.0", "knex": "^3.1.0", "pg": "^8.11.3", "sqlite3": "^5.1.7", @@ -12021,6 +12304,12 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "server/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, "server/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -12059,6 +12348,18 @@ "node": ">= 4" } }, + "server/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "server/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", diff --git a/package.json b/package.json index 7934438..df6c3ca 100644 --- a/package.json +++ b/package.json @@ -12,5 +12,11 @@ "test": "npm run test --workspaces", "lint": "npm run lint --workspaces", "lint:fix": "npm run lint:fix --workspaces" + }, + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@mui/icons-material": "^7.1.1", + "@mui/material": "^7.1.1" } } diff --git a/server/package.json b/server/package.json index 2507b94..99a2987 100644 --- a/server/package.json +++ b/server/package.json @@ -22,11 +22,13 @@ "@node-escpos/core": "^0.6.0", "@node-escpos/usb-adapter": "^0.3.1", "@task-receipts/shared": "file:../shared", + "@types/js-yaml": "^4.0.9", "apollo-server-express": "^3.13.0", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.18.3", "graphql": "^16.8.1", + "js-yaml": "^4.1.0", "knex": "^3.1.0", "pg": "^8.11.3", "sqlite3": "^5.1.7", diff --git a/server/src/__tests__/yaml-service.test.ts b/server/src/__tests__/yaml-service.test.ts new file mode 100644 index 0000000..74964e0 --- /dev/null +++ b/server/src/__tests__/yaml-service.test.ts @@ -0,0 +1,75 @@ +import { YamlService } from '../yaml-service'; +import { UserRepository } from '../db/repositories/user-repository'; +import knex from 'knex'; +import config from '../db/knexfile'; + +const MIGRATIONS_DIR = config.test.migrations?.directory; + +// Helper to truncate all tables +async function truncateAll(db: any) { + await db.raw('PRAGMA foreign_keys = OFF'); + await db('notes').truncate(); + await db('steps').truncate(); + await db('tasks').truncate(); + await db('groups').truncate(); + await db('users').truncate(); + await db('images').truncate(); + await db('print_history').truncate(); + await db.raw('PRAGMA foreign_keys = ON'); +} + +describe('YamlService', () => { + let db: ReturnType; + let yamlService: YamlService; + + beforeEach(async () => { + db = knex(config.test); + await db.migrate.latest({ directory: MIGRATIONS_DIR }); + // Create a user for notes + const userRepo = new UserRepository(db); + await userRepo.create({ name: 'Test User' }); + yamlService = new YamlService(db); + }); + + afterEach(async () => { + await db.destroy(); + }); + + it('should export empty database to YAML', async () => { + const yamlContent = await yamlService.exportToYaml(); + expect(yamlContent).toContain('groups: []'); + }); + + it('should import and export YAML correctly', async () => { + const testYaml = ` +groups: + - name: Test Group + tasks: + - name: Test Task + steps: + - name: Test Step + instructions: Test instructions + notes: + - content: Test note + `; + + // Import the YAML + await yamlService.importFromYaml(testYaml); + + // Export it back + const exportedYaml = await yamlService.exportToYaml(); + + // Should contain the imported data + expect(exportedYaml).toContain('Test Group'); + expect(exportedYaml).toContain('Test Task'); + expect(exportedYaml).toContain('Test Step'); + expect(exportedYaml).toContain('Test instructions'); + expect(exportedYaml).toContain('Test note'); + }); + + it('should handle invalid YAML', async () => { + const invalidYaml = 'invalid: yaml: content:'; + + await expect(yamlService.importFromYaml(invalidYaml)).rejects.toThrow(); + }); +}); \ No newline at end of file diff --git a/server/src/index.ts b/server/src/index.ts index 375b251..3a13ac2 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -8,16 +8,18 @@ import { createDb } from './db'; import logger from './logger'; import { createPrinter } from './printer'; import { PrintHistoryRepository, StepRepository } from './db/repositories'; +import { YamlService } from './yaml-service'; import cors from 'cors'; const app = express(); -const port = process.env.PORT || 4000; +const port = 4000; //process.env.PORT || 4000; async function startServer() { const db = createDb(); const printHistoryRepo = new PrintHistoryRepository(db); const stepRepo = new StepRepository(db); const printer = createPrinter(printHistoryRepo, stepRepo); + const yamlService = new YamlService(db); const server = new ApolloServer({ typeDefs, @@ -28,7 +30,9 @@ async function startServer() { // Enable CORS for the frontend app.use(cors({ - origin: 'http://localhost:5173', + // origin: 'http://localhost:5173', + // allow all origins + origin: '*', credentials: true })); @@ -40,8 +44,40 @@ async function startServer() { }) ); - const httpServer = app.listen(port, () => { + // YAML Export endpoint + app.get('/api/yaml/export', async (req, res) => { + try { + const yamlContent = await yamlService.exportToYaml(); + res.setHeader('Content-Type', 'text/yaml'); + res.setHeader('Content-Disposition', 'attachment; filename="database-export.yaml"'); + res.send(yamlContent); + } catch (error) { + logger.error('Error exporting YAML:', error); + res.status(500).json({ error: 'Failed to export YAML' }); + } + }); + + // YAML Import endpoint + app.post('/api/yaml/import', async (req, res) => { + try { + const { yamlContent } = req.body; + + if (!yamlContent || typeof yamlContent !== 'string') { + return res.status(400).json({ error: 'YAML content is required' }); + } + + await yamlService.importFromYaml(yamlContent); + res.json({ message: 'Database imported successfully' }); + } catch (error) { + logger.error('Error importing YAML:', error); + res.status(500).json({ error: 'Failed to import YAML' }); + } + }); + + const httpServer = app.listen(port, '0.0.0.0', () => { logger.info(`Server running at http://localhost:${port}/graphql`); + logger.info(`YAML export endpoint: http://localhost:${port}/api/yaml/export`); + logger.info(`YAML import endpoint: http://localhost:${port}/api/yaml/import`); }); // Graceful shutdown diff --git a/server/src/printer/format-utils.ts b/server/src/printer/format-utils.ts index 28f9f6f..d1df82e 100644 --- a/server/src/printer/format-utils.ts +++ b/server/src/printer/format-utils.ts @@ -1,3 +1,5 @@ +import { PAPER_CONFIG } from "./printer-constants"; + export const formatUtils = { /** * Creates a banner line with the specified character @@ -5,7 +7,7 @@ export const formatUtils = { * @param length Length of the banner * @returns Formatted banner string */ - createBanner(char: string, length: number = 40): string { + createBanner(char: string, length: number = PAPER_CONFIG.BANNER_LENGTH): string { return char.repeat(length); }, diff --git a/server/src/printer/serial-printer.ts b/server/src/printer/serial-printer.ts index 9d00e39..23982b9 100644 --- a/server/src/printer/serial-printer.ts +++ b/server/src/printer/serial-printer.ts @@ -111,7 +111,7 @@ export class SerialPrinter implements PrinterInterface { const step = taskSteps[i]; const stepSection = formatUtils.formatSection( formatUtils.formatStepHeader(step.name, i + 1, task.name, true), - step.instructions, + step.instructions || 'No instructions provided', '-' ); @@ -161,7 +161,7 @@ export class SerialPrinter implements PrinterInterface { const stepSection = formatUtils.formatSection( formatUtils.formatStepHeader(step.name, stepNumber, task?.name), - step.instructions + step.instructions || 'No instructions provided' ); await this.printer diff --git a/server/src/yaml-service.ts b/server/src/yaml-service.ts new file mode 100644 index 0000000..d43c0f2 --- /dev/null +++ b/server/src/yaml-service.ts @@ -0,0 +1,187 @@ +import * as yaml from 'js-yaml'; +import { Knex } from 'knex'; +import { GroupRepository, TaskRepository, StepRepository, NoteRepository } from './db/repositories'; +import { Group, Task, Step, Note } from '@shared/index'; + +export interface YamlData { + groups: YamlGroup[]; +} + +export interface YamlGroup { + name: string; + tasks: YamlTask[]; +} + +export interface YamlTask { + name: string; + steps: YamlStep[]; + notes?: YamlNote[]; +} + +export interface YamlStep { + name: string; + instructions: string; + notes?: YamlNote[]; +} + +export interface YamlNote { + content: string; +} + +export class YamlService { + private groupRepo: GroupRepository; + private taskRepo: TaskRepository; + private stepRepo: StepRepository; + private noteRepo: NoteRepository; + private db: Knex; + + constructor(db: Knex) { + this.db = db; + this.groupRepo = new GroupRepository(db); + this.taskRepo = new TaskRepository(db); + this.stepRepo = new StepRepository(db); + this.noteRepo = new NoteRepository(db); + } + + async exportToYaml(): Promise { + const groups = await this.groupRepo.findRootGroups(); + const yamlData: YamlData = { + groups: [] + }; + + for (const group of groups) { + const yamlGroup = await this.convertGroupToYaml(group); + yamlData.groups.push(yamlGroup); + } + + return yaml.dump(yamlData, { + indent: 2, + lineWidth: 120, + noRefs: true + }); + } + + async importFromYaml(yamlContent: string): Promise { + const yamlData = yaml.load(yamlContent) as YamlData; + + if (!yamlData || !yamlData.groups) { + throw new Error('Invalid YAML format: missing groups'); + } + + // Clear existing data (optional - you might want to make this configurable) + await this.clearExistingData(); + + // Import groups and their nested data + for (const yamlGroup of yamlData.groups) { + await this.importGroup(yamlGroup); + } + } + + private async convertGroupToYaml(group: Group): Promise { + const tasks = await this.taskRepo.findByGroupId(group.id); + const yamlTasks: YamlTask[] = []; + + for (const task of tasks) { + const yamlTask = await this.convertTaskToYaml(task); + yamlTasks.push(yamlTask); + } + + return { + name: group.name, + tasks: yamlTasks + }; + } + + private async convertTaskToYaml(task: Task): Promise { + const steps = await this.stepRepo.findByTaskId(task.id); + const notes = await this.noteRepo.findByTaskId(task.id); + + const yamlSteps: YamlStep[] = []; + for (const step of steps) { + const stepNotes = await this.noteRepo.findByStepId(step.id); + const yamlStep: YamlStep = { + name: step.name, + instructions: step.instructions, + notes: stepNotes.map(note => ({ content: note.content })) + }; + yamlSteps.push(yamlStep); + } + + const yamlTask: YamlTask = { + name: task.name, + steps: yamlSteps, + notes: notes.map(note => ({ content: note.content })) + }; + + return yamlTask; + } + + private async importGroup(yamlGroup: YamlGroup): Promise { + // Create the group + const group = await this.groupRepo.create({ + name: yamlGroup.name, + parent_id: undefined + }); + + // Import tasks for this group + for (const yamlTask of yamlGroup.tasks) { + await this.importTask(yamlTask, group.id); + } + } + + private async importTask(yamlTask: YamlTask, groupId: number): Promise { + // Create the task + const task = await this.taskRepo.create({ + name: yamlTask.name, + group_id: groupId, + print_count: 0 + }); + + // Import task-level notes + if (yamlTask.notes) { + for (const yamlNote of yamlTask.notes) { + await this.noteRepo.create({ + content: yamlNote.content, + task_id: task.id, + created_by: 1 // Default user ID + }); + } + } + + // Import steps + for (let i = 0; i < yamlTask.steps.length; i++) { + const yamlStep = yamlTask.steps[i]; + await this.importStep(yamlStep, task.id, i + 1); + } + } + + private async importStep(yamlStep: YamlStep, taskId: number, order: number): Promise { + // Create the step + const step = await this.stepRepo.create({ + name: yamlStep.name, + instructions: yamlStep.instructions, + task_id: taskId, + order: order, + print_count: 0 + }); + + // Import step-level notes + if (yamlStep.notes) { + for (const yamlNote of yamlStep.notes) { + await this.noteRepo.create({ + content: yamlNote.content, + step_id: step.id, + created_by: 1 // Default user ID + }); + } + } + } + + private async clearExistingData(): Promise { + // Delete in reverse order to respect foreign key constraints + await this.db('notes').del(); + await this.db('steps').del(); + await this.db('tasks').del(); + await this.db('groups').del(); + } +} \ No newline at end of file