Compare commits
28 Commits
f588461be1
...
dfacc39e57
| Author | SHA1 | Date | |
|---|---|---|---|
| dfacc39e57 | |||
| e125fb6e64 | |||
| 0a56fc6be0 | |||
| 3467a574c1 | |||
| adc20904cc | |||
| af84abe3f8 | |||
| 4c51822bf0 | |||
| 8e53710c90 | |||
| 4655ba4f64 | |||
| ead327d44f | |||
| a8ff28829d | |||
| 888d636be6 | |||
| 230ba670ab | |||
| fd763cc162 | |||
| 8b462f98f6 | |||
| 90233c7208 | |||
| ab2aca1d97 | |||
| fcee6e0b19 | |||
| 5e1e453b0b | |||
| a870e875c8 | |||
| 49b9389532 | |||
| 7c6045f1b3 | |||
| 7952e5632a | |||
| 92e426b316 | |||
| f095efd9e0 | |||
| 1290130d8e | |||
| 7be44bd8f2 | |||
| 0938dc861d |
23
angular.json
23
angular.json
@@ -27,6 +27,16 @@
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
},
|
||||
{
|
||||
"glob": "**/*.js",
|
||||
"input": "./node_modules/angular-web-sqlite/src/lib/assets",
|
||||
"output": "./sqlite-client/"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "./node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/",
|
||||
"output": "./sqlite-client/"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
@@ -67,10 +77,19 @@
|
||||
"buildTarget": "groceries-price-tracker:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
"defaultConfiguration": "development",
|
||||
"options": {
|
||||
"headers": {
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
"Cross-Origin-Embedder-Policy": "require-corp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular/build:unit-test"
|
||||
"builder": "@angular/build:unit-test",
|
||||
"options": {
|
||||
"coverageExclude": ["**/*.html"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
cert/server.crt
Normal file
25
cert/server.crt
Normal file
@@ -0,0 +1,25 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIESzCCAzOgAwIBAgIUeHJl5gcLEDqZCFSNISmGK1L7HQswDQYJKoZIhvcNAQEL
|
||||
BQAwgbQxCzAJBgNVBAYTAkFSMRUwEwYDVQQIDAxCdWVub3MgQWlyZXMxFTATBgNV
|
||||
BAcMDEJ1ZW5vcyBBaXJlczETMBEGA1UECgwKR2FiaWxhbmRpYTEeMBwGA1UECwwV
|
||||
R2FiaWxhbmRpYSBkZXZlbG9wZXJzMRMwEQYDVQQDDApHYWJpbGFuZGlhMS0wKwYJ
|
||||
KoZIhvcNAQkBFh5nYWJyaWVsLmRlbG9zcmlvc0B0dXRhbWFpbC5jb20wHhcNMjUx
|
||||
MjI3MjMyOTM1WhcNMjYxMjI3MjMyOTM1WjCBtDELMAkGA1UEBhMCQVIxFTATBgNV
|
||||
BAgMDEJ1ZW5vcyBBaXJlczEVMBMGA1UEBwwMQnVlbm9zIEFpcmVzMRMwEQYDVQQK
|
||||
DApHYWJpbGFuZGlhMR4wHAYDVQQLDBVHYWJpbGFuZGlhIGRldmVsb3BlcnMxEzAR
|
||||
BgNVBAMMCkdhYmlsYW5kaWExLTArBgkqhkiG9w0BCQEWHmdhYnJpZWwuZGVsb3Ny
|
||||
aW9zQHR1dGFtYWlsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
|
||||
AJzeRKX60Aidx2f9Vej9JagIG3B3dPMt5igVCbw8rra6k5/kUH0Vyk4TLfI9UBLX
|
||||
7T3i0c3pYZPaBm1dimNRkZEKY8QauwGfGuSR1hhyr4vwhhLaAXqnhm7CZ5fwN+PA
|
||||
WySfEqXDuyeURSLvaHdNztLFMaDnA1C0cq0waQhJTVe1xMmcFkW76qRWEkKb30oq
|
||||
FGF5GNYKVXfrzWYD4ZxW98tGrz6AhgR6XkF+aSmotKoJn6mUWQnmsnjKmBRXcaqx
|
||||
j6lJgcpdxWh8FitBB2fLUOnOZ9avcDZv9wVvVZehUgcHiAOvSIAvANdS3bKFrQcc
|
||||
6ZV/WEI0ZRQTqZ8hXufXW+kCAwEAAaNTMFEwHQYDVR0OBBYEFDyTMsJj8YEoJcAe
|
||||
FJ1LzpuV596+MB8GA1UdIwQYMBaAFDyTMsJj8YEoJcAeFJ1LzpuV596+MA8GA1Ud
|
||||
EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAI0UY4KLPzlc1y6VR6flFsKA
|
||||
trTBdkyICoXf2rSnRuSNS9c9cQN6j9MC3D3oymw1e3NftCh0a/Mem5A24hwK0CPQ
|
||||
ngGoGJZrfj5QHAoJ2pmIrlsmBTL66ZkvRYll/kwKHUZ3ePjaQp+JafAuhxABFhaG
|
||||
geC+c9gzSe3cIPuU6dd14AyFJewRu+VqY4fKIoHl2kwvdxzTe5/Fua6b084dbZMs
|
||||
GaEfYMkp/BuSLZVpbkliE6qq2w9qYW9yMEp9DA7nsQW+bauayTgcVWswJCzCZop2
|
||||
zaoVfNI3cyUIFDQxFNmr8uyZc9bQ+boeZ9NB1yKIWE+0RP4LrznuirGtCpHZiuo=
|
||||
-----END CERTIFICATE-----
|
||||
28
cert/server.key
Normal file
28
cert/server.key
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCc3kSl+tAIncdn
|
||||
/VXo/SWoCBtwd3TzLeYoFQm8PK62upOf5FB9FcpOEy3yPVAS1+094tHN6WGT2gZt
|
||||
XYpjUZGRCmPEGrsBnxrkkdYYcq+L8IYS2gF6p4ZuwmeX8DfjwFsknxKlw7snlEUi
|
||||
72h3Tc7SxTGg5wNQtHKtMGkISU1XtcTJnBZFu+qkVhJCm99KKhRheRjWClV3681m
|
||||
A+GcVvfLRq8+gIYEel5BfmkpqLSqCZ+plFkJ5rJ4ypgUV3GqsY+pSYHKXcVofBYr
|
||||
QQdny1DpzmfWr3A2b/cFb1WXoVIHB4gDr0iALwDXUt2yha0HHOmVf1hCNGUUE6mf
|
||||
IV7n11vpAgMBAAECggEABkNhnkxAriIOlM+qnx4sY2R0zq1rqqH0hI6Cev+Ol5Z6
|
||||
JRzVPIlNhdyCLcqA6J+ycJFz1bLccIN4qjkyjftIbUVq7P4xj+5YVolJcQ8d2d/a
|
||||
mo9rFl3woiTPmg4UooVK86VeSzwLZu3RQi8o4/U5WB+uIZctAgs9C5PZPbl02xbu
|
||||
KYO6ByNB/fksELlJGrw+46hjDxg53a6TPOtDgqGlyIweJIS3LyznvcP1qEYoXP90
|
||||
kgFu141hC4HRYprYTXykuUx8e5y/QmOQdM6of6vkT2SVFyN4YYupUsKJpRrHNDJM
|
||||
WcjXVVLs8vYhHiDJ41MnV4zoHUNgc78fCn/RE2l0AQKBgQDLGW5C5vEsC4cAQWVR
|
||||
ppCED5MtHblEH153MxXyNnnrIzCsjVCL3HQtsCcxGFxXyTMCEqpc1HdwYsMAn/PJ
|
||||
vx8W06VdHIoUkKUDf+nvVvlSdiUBWZONKOJDG06d10YdVOWr1xB7UyRMz+KE6yxe
|
||||
svIYDs53TQckH6fGx8ADAsUPwQKBgQDFuitC8obNWOgtaJb4NhbDZh9xSDQDyd3M
|
||||
CoHiTGDjviQYrN0sUs05w4bB/b/gH8vUsL3xJ6ajo1k6iSxCEyKYFPwVzG00nNz9
|
||||
oZQD0HjmtyU2Ht9Gz1OidXUkUT1/zXG0YVWTw3xcL1CkBVHeddnPwu6+LWItaI9b
|
||||
BOMhAj1WKQKBgF0doLbdqQ73jgKo+Onxgup5NZIGwa0g8K+X5WTyYv1SWfuSoq4s
|
||||
+bsEu0NAFv5Mia1Wn3MGGmiVbzA3JY+Gp5tQl81Ty17YXj93guqvpomPDzJKPmMG
|
||||
ro3z1Bx72XKTPOWHKdBQ8yCgYwtrwyD7zBEJoNGDqFWAieySIk9/EphBAoGAHnT5
|
||||
2vsetpzeTrhQoPU79mdRqpJzqK28o4Ru01vuhMYyKzbdbslmYWJz7IfYuX5MWHvN
|
||||
FkuESKqNwQ2GKrtl1cVNu1Hc9IDBLbRo51mCdg96BOcmf3LKMDkljS2SapBL1nwz
|
||||
wWoYSt7i7hD/tmOy5GTjf5ngCJyVkHClR91fc1kCgYAvlB1BodeZApt1dFjLEz8F
|
||||
l9/VzBZrMNf1Py5rHl2wFT+xM/4gXArjvKkBSCO9dqRbvmfvFTBVuI+nd2a0CoVH
|
||||
dtfc/4GzGNHzjcocScZG+UIDMLvDXpPZicEGcc2IERjlopDH/YuBlYsBd5n+19zK
|
||||
ZA5FJdXg9HW/iJMhuLyh6A==
|
||||
-----END PRIVATE KEY-----
|
||||
333
package-lock.json
generated
333
package-lock.json
generated
@@ -14,6 +14,7 @@
|
||||
"@angular/forms": "^21.0.0",
|
||||
"@angular/platform-browser": "^21.0.0",
|
||||
"@angular/router": "^21.0.0",
|
||||
"angular-web-sqlite": "^1.0.34",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -21,6 +22,7 @@
|
||||
"@angular/build": "^21.0.4",
|
||||
"@angular/cli": "^21.0.4",
|
||||
"@angular/compiler-cli": "^21.0.0",
|
||||
"@vitest/coverage-v8": "^4.0.17",
|
||||
"jsdom": "^27.1.0",
|
||||
"typescript": "~5.9.2",
|
||||
"vitest": "^4.0.8"
|
||||
@@ -948,6 +950,16 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bcoe/v8-coverage": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
|
||||
"integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
|
||||
@@ -1927,6 +1939,41 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@ionic/angular": {
|
||||
"version": "8.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-8.7.15.tgz",
|
||||
"integrity": "sha512-2FFmOCoE3CRHR/WAsx+DX084ywxCNEdqrDuqy0iUmi1jbXrYFcC3xnbvBLKRsQtN56nFLS07DkL6wLCCOY7M4A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ionic/core": "8.7.15",
|
||||
"ionicons": "^8.0.13",
|
||||
"jsonc-parser": "^3.0.0",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": ">=16.0.0",
|
||||
"@angular/forms": ">=16.0.0",
|
||||
"@angular/router": ">=16.0.0",
|
||||
"rxjs": ">=7.5.0",
|
||||
"zone.js": ">=0.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ionic/core": {
|
||||
"version": "8.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.15.tgz",
|
||||
"integrity": "sha512-u1w9c6dx2iuatXIW5X1JY0ighDhQPjBwOHZsrOcnpm891pktuEjJDdyhDulWFa6kKVkXw1q7khwxXBEurvKc2g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@stencil/core": "4.38.0",
|
||||
"ionicons": "^8.0.13",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
@@ -3899,12 +3946,45 @@
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sqlite.org/sqlite-wasm": {
|
||||
"version": "3.44.0-build2",
|
||||
"resolved": "https://registry.npmjs.org/@sqlite.org/sqlite-wasm/-/sqlite-wasm-3.44.0-build2.tgz",
|
||||
"integrity": "sha512-zFfqHoYxuGQ15+A9W00JGQEfQ+f1b+NNpZRD98I31G0x7es+IiGIh8n52vBoub+q9bZLW++Xa6T1QXuw4eGSDQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"sqlite-wasm": "bin/index.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@stencil/core": {
|
||||
"version": "4.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.38.0.tgz",
|
||||
"integrity": "sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"stencil": "bin/stencil"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0",
|
||||
"npm": ">=7.10.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-darwin-arm64": "4.34.9",
|
||||
"@rollup/rollup-darwin-x64": "4.34.9",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.34.9",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.34.9",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.34.9",
|
||||
"@rollup/rollup-linux-x64-musl": "4.34.9",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.34.9",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.34.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@tufjs/canonical-json": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
|
||||
@@ -3978,17 +4058,48 @@
|
||||
"vite": "^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.17.tgz",
|
||||
"integrity": "sha512-/6zU2FLGg0jsd+ePZcwHRy3+WpNTBBhDY56P4JTRqUN/Dp6CvOEa9HrikcQ4KfV2b2kAHUFB4dl1SuocWXSFEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@bcoe/v8-coverage": "^1.0.2",
|
||||
"@vitest/utils": "4.0.17",
|
||||
"ast-v8-to-istanbul": "^0.3.10",
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"istanbul-lib-report": "^3.0.1",
|
||||
"istanbul-reports": "^3.2.0",
|
||||
"magicast": "^0.5.1",
|
||||
"obug": "^2.1.1",
|
||||
"std-env": "^3.10.0",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vitest/browser": "4.0.17",
|
||||
"vitest": "4.0.17"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vitest/browser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.16.tgz",
|
||||
"integrity": "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==",
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.17.tgz",
|
||||
"integrity": "sha512-mEoqP3RqhKlbmUmntNDDCJeTDavDR+fVYkSOw8qRwJFaW/0/5zA9zFeTrHqNtcmwh6j26yMmwx2PqUDPzt5ZAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/spy": "4.0.16",
|
||||
"@vitest/utils": "4.0.16",
|
||||
"@vitest/spy": "4.0.17",
|
||||
"@vitest/utils": "4.0.17",
|
||||
"chai": "^6.2.1",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
@@ -3997,13 +4108,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz",
|
||||
"integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==",
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.17.tgz",
|
||||
"integrity": "sha512-+ZtQhLA3lDh1tI2wxe3yMsGzbp7uuJSWBM1iTIKCbppWTSBN09PUC+L+fyNlQApQoR+Ps8twt2pbSSXg2fQVEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "4.0.16",
|
||||
"@vitest/spy": "4.0.17",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.21"
|
||||
},
|
||||
@@ -4034,9 +4145,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/pretty-format": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.16.tgz",
|
||||
"integrity": "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==",
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.17.tgz",
|
||||
"integrity": "sha512-Ah3VAYmjcEdHg6+MwFE17qyLqBHZ+ni2ScKCiW2XrlSBV4H3Z7vYfPfz7CWQ33gyu76oc0Ai36+kgLU3rfF4nw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4047,13 +4158,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.16.tgz",
|
||||
"integrity": "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==",
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.17.tgz",
|
||||
"integrity": "sha512-JmuQyf8aMWoo/LmNFppdpkfRVHJcsgzkbCA+/Bk7VfNH7RE6Ut2qxegeyx2j3ojtJtKIbIGy3h+KxGfYfk28YQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.0.16",
|
||||
"@vitest/utils": "4.0.17",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
@@ -4061,13 +4172,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.16.tgz",
|
||||
"integrity": "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==",
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.17.tgz",
|
||||
"integrity": "sha512-npPelD7oyL+YQM2gbIYvlavlMVWUfNNGZPcu0aEUQXt7FXTuqhmgiYupPnAanhKvyP6Srs2pIbWo30K0RbDtRQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.0.16",
|
||||
"@vitest/pretty-format": "4.0.17",
|
||||
"magic-string": "^0.30.21",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
@@ -4086,9 +4197,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.16.tgz",
|
||||
"integrity": "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==",
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.17.tgz",
|
||||
"integrity": "sha512-I1bQo8QaP6tZlTomQNWKJE6ym4SHf3oLS7ceNjozxxgzavRAgZDc06T7kD8gb9bXKEgcLNt00Z+kZO6KaJ62Ew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -4096,13 +4207,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.16.tgz",
|
||||
"integrity": "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==",
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.17.tgz",
|
||||
"integrity": "sha512-RG6iy+IzQpa9SB8HAFHJ9Y+pTzI+h8553MrciN9eC6TFBErqrQaTas4vG+MVj8S4uKk8uTT2p0vgZPnTdxd96w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.0.16",
|
||||
"@vitest/pretty-format": "4.0.17",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
@@ -4211,6 +4322,22 @@
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/angular-web-sqlite": {
|
||||
"version": "1.0.34",
|
||||
"resolved": "https://registry.npmjs.org/angular-web-sqlite/-/angular-web-sqlite-1.0.34.tgz",
|
||||
"integrity": "sha512-OfV/vA8FEBfb2SjbPB5529ZcjalAUf6WiI+RZcW3V/N1OgRNbM7HrxmIuYJCQ2cm8ZYO6LgOQRmN+ufeAIdM1g==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sqlite.org/sqlite-wasm": "3.44.0-build2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "13.x || 14.x || 15.x || 16.x || 17.x || 18.x || 19.x || 20.x || 21.x || 22.x",
|
||||
"@angular/core": "13.x || 14.x || 15.x || 16.x || 17.x || 18.x || 19.x || 20.x || 21.x || 22.x",
|
||||
"@ionic/angular": "5.x || 6.x || 7.x || 8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-escapes": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz",
|
||||
@@ -4263,6 +4390,25 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-v8-to-istanbul": {
|
||||
"version": "0.3.10",
|
||||
"resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.10.tgz",
|
||||
"integrity": "sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.31",
|
||||
"estree-walker": "^3.0.3",
|
||||
"js-tokens": "^9.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-v8-to-istanbul/node_modules/js-tokens": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
|
||||
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.9.11",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
|
||||
@@ -5539,6 +5685,16 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
@@ -5601,6 +5757,13 @@
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/htmlparser2": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz",
|
||||
@@ -5754,6 +5917,16 @@
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ionicons": {
|
||||
"version": "8.0.13",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-8.0.13.tgz",
|
||||
"integrity": "sha512-2QQVyG2P4wszne79jemMjWYLp0DBbDhr4/yFroPCxvPP1wtMxgdIV3l5n+XZ5E9mgoXU79w7yTWpm2XzJsISxQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@stencil/core": "^4.35.3"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||
@@ -5916,6 +6089,35 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-report": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
|
||||
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"istanbul-lib-coverage": "^3.0.0",
|
||||
"make-dir": "^4.0.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-reports": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
|
||||
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"html-escaper": "^2.0.0",
|
||||
"istanbul-lib-report": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz",
|
||||
@@ -6020,7 +6222,6 @@
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
|
||||
"integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsonparse": {
|
||||
@@ -6208,6 +6409,34 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/magicast": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz",
|
||||
"integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@babel/types": "^7.28.5",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
|
||||
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/make-fetch-happen": {
|
||||
"version": "15.0.3",
|
||||
"resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.3.tgz",
|
||||
@@ -7983,6 +8212,19 @@
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-preserve-symlinks-flag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
@@ -8862,19 +9104,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz",
|
||||
"integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==",
|
||||
"version": "4.0.17",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.17.tgz",
|
||||
"integrity": "sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.16",
|
||||
"@vitest/mocker": "4.0.16",
|
||||
"@vitest/pretty-format": "4.0.16",
|
||||
"@vitest/runner": "4.0.16",
|
||||
"@vitest/snapshot": "4.0.16",
|
||||
"@vitest/spy": "4.0.16",
|
||||
"@vitest/utils": "4.0.16",
|
||||
"@vitest/expect": "4.0.17",
|
||||
"@vitest/mocker": "4.0.17",
|
||||
"@vitest/pretty-format": "4.0.17",
|
||||
"@vitest/runner": "4.0.17",
|
||||
"@vitest/snapshot": "4.0.17",
|
||||
"@vitest/spy": "4.0.17",
|
||||
"@vitest/utils": "4.0.17",
|
||||
"es-module-lexer": "^1.7.0",
|
||||
"expect-type": "^1.2.2",
|
||||
"magic-string": "^0.30.21",
|
||||
@@ -8902,10 +9144,10 @@
|
||||
"@edge-runtime/vm": "*",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||
"@vitest/browser-playwright": "4.0.16",
|
||||
"@vitest/browser-preview": "4.0.16",
|
||||
"@vitest/browser-webdriverio": "4.0.16",
|
||||
"@vitest/ui": "4.0.16",
|
||||
"@vitest/browser-playwright": "4.0.17",
|
||||
"@vitest/browser-preview": "4.0.17",
|
||||
"@vitest/browser-webdriverio": "4.0.17",
|
||||
"@vitest/ui": "4.0.17",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
@@ -9291,6 +9533,13 @@
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25 || ^4"
|
||||
}
|
||||
},
|
||||
"node_modules/zone.js": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.16.0.tgz",
|
||||
"integrity": "sha512-LqLPpIQANebrlxY6jKcYKdgN5DTXyyHAKnnWWjE5pPfEQ4n7j5zn7mOEEpwNZVKGqx3kKKmvplEmoBrvpgROTA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"start": "ng serve --host 0.0.0.0 --ssl true --ssl-key \"./cert/server.key\" --ssl-cert \"./cert/server.crt\"",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
@@ -29,6 +29,7 @@
|
||||
"@angular/forms": "^21.0.0",
|
||||
"@angular/platform-browser": "^21.0.0",
|
||||
"@angular/router": "^21.0.0",
|
||||
"angular-web-sqlite": "^1.0.34",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -36,6 +37,7 @@
|
||||
"@angular/build": "^21.0.4",
|
||||
"@angular/cli": "^21.0.4",
|
||||
"@angular/compiler-cli": "^21.0.0",
|
||||
"@vitest/coverage-v8": "^4.0.17",
|
||||
"jsdom": "^27.1.0",
|
||||
"typescript": "~5.9.2",
|
||||
"vitest": "^4.0.8"
|
||||
|
||||
126
public/barcode-reader/barcode-reader.css
Normal file
126
public/barcode-reader/barcode-reader.css
Normal file
@@ -0,0 +1,126 @@
|
||||
:host {
|
||||
display: flex;
|
||||
--video-width: 100%;
|
||||
--video-height: 100dvh;
|
||||
--video-background-color: #cccccc;
|
||||
--dialog-background-color: #ffffff;
|
||||
--dialog-backdrop-background-color: #cecece;
|
||||
}
|
||||
|
||||
#container {
|
||||
display: none;
|
||||
position: relative;
|
||||
width: var(--video-width);
|
||||
background-color: #000000;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border-radius: 50%;
|
||||
border-style: none;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-right: 12px;
|
||||
margin-top: 12px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
#container {
|
||||
min-width: 400px;
|
||||
min-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
#video-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: var(--video-height);
|
||||
max-height: var(--video-height);
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
canvas {
|
||||
position: absolute;
|
||||
top: 30%;
|
||||
width: 100%;
|
||||
height: 25%;
|
||||
}
|
||||
|
||||
#data-canvas {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#frame {
|
||||
opacity: 1;
|
||||
position: absolute;
|
||||
top: 30%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 25%;
|
||||
background-color: transparent;
|
||||
transform-origin: center center;
|
||||
transform: scale(0.9);
|
||||
transition: transform .3s ease-out;
|
||||
}
|
||||
|
||||
:host([capturing]) #frame {
|
||||
transform: scale(0.5);
|
||||
}
|
||||
|
||||
:host([no-frame]) #frame {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.corner {
|
||||
position: absolute;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 3px solid #ee0b0b;
|
||||
clip-path: polygon(0 48px, 48px 48px, 48px 0, 0 0);
|
||||
}
|
||||
|
||||
.top-left {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.top-right {
|
||||
top: 0;
|
||||
right: 0;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.bottom-right {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.bottom-left {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 31%;
|
||||
left: 2%;
|
||||
width: 96%;
|
||||
height: 23%;
|
||||
border-radius: 2%;
|
||||
z-index: 100;
|
||||
box-shadow: 0 0 0 200vmax rgba(0, 0, 0, 0.6);
|
||||
pointer-events: none;
|
||||
}
|
||||
1034
public/scan-loading-screen/scan-loading-screen.js
Normal file
1034
public/scan-loading-screen/scan-loading-screen.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,21 @@
|
||||
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
|
||||
import { ApplicationConfig, inject, provideAppInitializer, provideBrowserGlobalErrorListeners } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
import { WebSqlite } from 'angular-web-sqlite';
|
||||
import { Sqlite } from './services/sqlite';
|
||||
import { tables } from '../migrations/20260117';
|
||||
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideAppInitializer(async () => {
|
||||
const sqlite = inject(Sqlite);
|
||||
await sqlite.initializeDatabase('gptdb');
|
||||
await sqlite.batchSqlOperations(tables);
|
||||
await sqlite.executeQuery('PRAGMA foreign_keys = ON;');
|
||||
document.dispatchEvent(new CustomEvent('ng-boot'));
|
||||
}),
|
||||
{provide: WebSqlite, useClass: WebSqlite},
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
provideRouter(routes)
|
||||
]
|
||||
|
||||
@@ -14,10 +14,4 @@ describe('App', () => {
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render title', async () => {
|
||||
const fixture = TestBed.createComponent(App);
|
||||
await fixture.whenStable();
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, groceries-price-tracker');
|
||||
});
|
||||
});
|
||||
|
||||
6
src/app/components/bar-code-reader/bar-code-reader.html
Normal file
6
src/app/components/bar-code-reader/bar-code-reader.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<barcode-reader
|
||||
#reader
|
||||
(result)="this.result.emit($event)"
|
||||
(scan-status)="this.scanStatus.emit($event)"
|
||||
(scan-permission-denied)="this.scanPermissionDenied.emit()"
|
||||
/>
|
||||
6
src/app/components/bar-code-reader/bar-code-reader.scss
Normal file
6
src/app/components/bar-code-reader/bar-code-reader.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
barcode-reader {
|
||||
display: block;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
23
src/app/components/bar-code-reader/bar-code-reader.spec.ts
Normal file
23
src/app/components/bar-code-reader/bar-code-reader.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BarCodeReaderWrapper } from './bar-code-reader';
|
||||
|
||||
describe('BarCodeReaderWrapper', () => {
|
||||
let component: BarCodeReaderWrapper;
|
||||
let fixture: ComponentFixture<BarCodeReaderWrapper>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [BarCodeReaderWrapper]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(BarCodeReaderWrapper);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
23
src/app/components/bar-code-reader/bar-code-reader.ts
Normal file
23
src/app/components/bar-code-reader/bar-code-reader.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Component, CUSTOM_ELEMENTS_SCHEMA, effect, ElementRef, input, output, signal, viewChild } from '@angular/core';
|
||||
import { DetectedBarcode } from '../../types/globalThis';
|
||||
import { BarcodeReader } from '../../web-components/barcode-reader';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bar-code-reader',
|
||||
templateUrl: './bar-code-reader.html',
|
||||
styleUrl: './bar-code-reader.scss',
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
})
|
||||
export class BarCodeReaderWrapper {
|
||||
private readonly ref = viewChild<ElementRef<BarcodeReader>>('reader');
|
||||
protected readonly result = output<Event & {detail?: {code: DetectedBarcode | null}}>();
|
||||
protected readonly scanStatus = output<Event & {detail?: {state: ScanState}}>();
|
||||
protected readonly scanPermissionDenied = output();
|
||||
scan = input({scan: false})
|
||||
|
||||
constructor() {
|
||||
effect(() => this.scan().scan && this.ref()?.nativeElement.scan())
|
||||
}
|
||||
}
|
||||
|
||||
export type ScanState = 'loading'|'started'|'stopped';
|
||||
@@ -0,0 +1 @@
|
||||
<scan-loading-screen></scan-loading-screen>
|
||||
@@ -0,0 +1,7 @@
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 999;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ScanLoadingScreen } from './scan-loading-screen';
|
||||
|
||||
describe('ScanLoadingScreen', () => {
|
||||
let component: ScanLoadingScreen;
|
||||
let fixture: ComponentFixture<ScanLoadingScreen>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ScanLoadingScreen]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ScanLoadingScreen);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-scan-loading-screen',
|
||||
imports: [],
|
||||
templateUrl: './scan-loading-screen.html',
|
||||
styleUrl: './scan-loading-screen.scss',
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class ScanLoadingScreen {
|
||||
|
||||
}
|
||||
86
src/app/dao/BaseDAO.ts
Normal file
86
src/app/dao/BaseDAO.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { Sqlite } from '../services/sqlite';
|
||||
import { QueryResult } from '../types/sqlite.type';
|
||||
|
||||
export abstract class BaseDAO<T extends Object, K extends Object, U> {
|
||||
protected readonly sqlite = inject(Sqlite);
|
||||
protected readonly tableName: string;
|
||||
|
||||
constructor(tableName: string) {
|
||||
this.tableName = tableName.toLowerCase();
|
||||
}
|
||||
|
||||
async insert(entity: T) {
|
||||
const params = [];
|
||||
let sql = `INSERT INTO ${this.tableName} ( `;
|
||||
let values = `VALUES ( `;
|
||||
let dbModel = this.toDB(entity);
|
||||
for (let key in dbModel) {
|
||||
if (dbModel[key] !== undefined) {
|
||||
sql += `${key}, `;
|
||||
params.push(dbModel[key]);
|
||||
values += '?, ';
|
||||
}
|
||||
}
|
||||
sql = sql.slice(0, -2);
|
||||
sql += ' ) ';
|
||||
values = values.slice(0, -2);
|
||||
values += ' );';
|
||||
sql = sql + values;
|
||||
const result: { rows: never[] } = await this.sqlite.executeQuery(sql, params);
|
||||
return result;
|
||||
}
|
||||
|
||||
async findBy(values: Partial<K>) {
|
||||
let sql = `SELECT * FROM ${this.tableName} `;
|
||||
const params: any[] = [];
|
||||
const whereSql = this.whereClauseGenerator(values, params);
|
||||
sql += whereSql;
|
||||
sql += ';';
|
||||
const queryResults: QueryResult<U> = await this.sqlite.executeQuery(sql, params);
|
||||
return queryResults.rows.map(this.fromDB);
|
||||
}
|
||||
|
||||
async findAll() {
|
||||
const queryResults: QueryResult<U> = await this.sqlite.executeQuery(
|
||||
`SELECT * FROM ${this.tableName.toLowerCase()}`
|
||||
);
|
||||
return queryResults.rows.map(this.fromDB);
|
||||
}
|
||||
|
||||
async update(values: T, where: Partial<K>) {
|
||||
const params: any[] = [];
|
||||
let sql = `UPDATE ${this.tableName} SET `;
|
||||
let updates = '';
|
||||
let dbModel = this.toDB(values);
|
||||
for (let key in dbModel) {
|
||||
if (dbModel[key] !== undefined) {
|
||||
updates += `${key} = ?, `;
|
||||
params.push(dbModel[key]);
|
||||
}
|
||||
}
|
||||
updates = updates.slice(0, -2);
|
||||
updates += ' ';
|
||||
sql += updates;
|
||||
const whereSql = this.whereClauseGenerator(where, params);
|
||||
sql += whereSql + ';';
|
||||
const result: { rows: never[] } = await this.sqlite.executeQuery(sql, params);
|
||||
return result;
|
||||
}
|
||||
|
||||
protected whereClauseGenerator(values: Partial<K>, params: any[], alias?: string) {
|
||||
let sql = `WHERE `;
|
||||
const objLength = Object.keys(values).length;
|
||||
let i = 0;
|
||||
for (let key in values) {
|
||||
i += 1;
|
||||
sql += `${alias ? alias + '.' : ''}${key} = ? `;
|
||||
params.push(values[key]);
|
||||
if (i < objLength) sql += `AND `;
|
||||
}
|
||||
return sql;
|
||||
}
|
||||
|
||||
protected abstract toDB(model: T): K;
|
||||
protected abstract fromDB(queryResult: U): T;
|
||||
}
|
||||
73
src/app/dao/ChainDAO.spec.ts
Normal file
73
src/app/dao/ChainDAO.spec.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ChainDAO } from './ChainDAO';
|
||||
import { Sqlite } from '../services/sqlite';
|
||||
import { vi } from 'vitest';
|
||||
import { QueryResult } from '../types/sqlite.type';
|
||||
import { Chain } from '../models/Chain';
|
||||
|
||||
describe('ChainDAO', () => {
|
||||
let service: ChainDAO;
|
||||
let sqlite: Partial<Sqlite>;
|
||||
let CHAIN_MOCK: Chain;
|
||||
let RESULTS_MOCK: QueryResult<Chain>;
|
||||
|
||||
beforeEach(() => {
|
||||
CHAIN_MOCK = new Chain('mock', 'mock.png', 1);
|
||||
RESULTS_MOCK = { rows: [CHAIN_MOCK] };
|
||||
|
||||
sqlite = {
|
||||
executeQuery: vi.fn().mockResolvedValue(RESULTS_MOCK),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{ provide: Sqlite, useValue: sqlite }],
|
||||
});
|
||||
|
||||
service = TestBed.inject(ChainDAO);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should receive mapped results on findAll', async () => {
|
||||
const result = await service.findAll();
|
||||
if (result.length > 0) {
|
||||
expect(result[0]).toEqual(CHAIN_MOCK);
|
||||
} else {
|
||||
test.fails('Expected mock response to contain at least one value');
|
||||
}
|
||||
});
|
||||
|
||||
it('should receive mapped results on findBy', async () => {
|
||||
const result = await service.findBy({ id: CHAIN_MOCK.id, name: CHAIN_MOCK.name });
|
||||
if (result.length > 0) {
|
||||
expect(result[0]).toEqual(CHAIN_MOCK);
|
||||
} else {
|
||||
test.fails('Expected mock response to contain at least one value');
|
||||
}
|
||||
});
|
||||
|
||||
it('should call executeQuery with object fields on insert', async () => {
|
||||
const expectedSql = 'INSERT INTO chain ( name, image, id ) VALUES ( ?, ?, ? );';
|
||||
const expectedParams: any[] = [];
|
||||
for (let key in CHAIN_MOCK) {
|
||||
expectedParams.push(CHAIN_MOCK[<keyof Chain>key]);
|
||||
}
|
||||
await service.insert(CHAIN_MOCK);
|
||||
expect(sqlite.executeQuery).toHaveBeenCalledExactlyOnceWith(expectedSql, expectedParams);
|
||||
});
|
||||
|
||||
it('should call executeQuery with object fields and where values on update', async () => {
|
||||
const expectedParams: any[] = [];
|
||||
CHAIN_MOCK.name = 'Updated mock';
|
||||
for (let key in CHAIN_MOCK) {
|
||||
expectedParams.push(CHAIN_MOCK[<keyof Chain>key]);
|
||||
}
|
||||
const WHERE_MOCK: Partial<Chain> = { id: 1 };
|
||||
expectedParams.push(WHERE_MOCK.id);
|
||||
const expectedSql = 'UPDATE chain SET name = ?, image = ?, id = ? WHERE id = ? ;';
|
||||
await service.update(CHAIN_MOCK, WHERE_MOCK);
|
||||
expect(sqlite.executeQuery).toHaveBeenCalledExactlyOnceWith(expectedSql, expectedParams);
|
||||
});
|
||||
});
|
||||
21
src/app/dao/ChainDAO.ts
Normal file
21
src/app/dao/ChainDAO.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Chain } from '../models/Chain';
|
||||
import { BaseDAO } from './BaseDAO';
|
||||
import { DBChain } from '../models/db/DBChain';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ChainDAO extends BaseDAO<Chain, DBChain, DBChain> {
|
||||
constructor() {
|
||||
super(Chain.name);
|
||||
}
|
||||
|
||||
override toDB(model: Chain): DBChain {
|
||||
return new DBChain(model.name, model.image, model.id);
|
||||
}
|
||||
|
||||
override fromDB(qResult: DBChain): Chain {
|
||||
return new Chain(qResult.name, qResult.image, qResult.id);
|
||||
}
|
||||
}
|
||||
27
src/app/dao/ComposedDAO.ts
Normal file
27
src/app/dao/ComposedDAO.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { QueryResult } from '../types/sqlite.type';
|
||||
import { BaseDAO } from './BaseDAO';
|
||||
|
||||
export abstract class ComposedDAO<T extends Object, K extends Object, U> extends BaseDAO<T, K, U> {
|
||||
protected readonly JOIN_SQL: string;
|
||||
protected readonly tableAlias: string;
|
||||
|
||||
constructor(tableName: string, joinSql: string, tableAlias: string) {
|
||||
super(tableName);
|
||||
this.JOIN_SQL = joinSql;
|
||||
this.tableAlias = tableAlias;
|
||||
}
|
||||
|
||||
override async findAll() {
|
||||
const result: QueryResult<U> = await this.sqlite.executeQuery(this.JOIN_SQL);
|
||||
return result.rows.map(this.fromDB);
|
||||
}
|
||||
|
||||
override async findBy(values: Partial<K>): Promise<T[]> {
|
||||
let sql = this.JOIN_SQL.slice(0, -1) + ' ';
|
||||
const params: any[] = [];
|
||||
const whereSql = this.whereClauseGenerator(values, params, this.tableAlias);
|
||||
sql += whereSql + ';';
|
||||
const result: QueryResult<U> = await this.sqlite.executeQuery(sql, params);
|
||||
return result.rows.map(this.fromDB);
|
||||
}
|
||||
}
|
||||
76
src/app/dao/EstablishmentDAO.spec.ts
Normal file
76
src/app/dao/EstablishmentDAO.spec.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { EstablishmentDAO } from './EstablishmentDAO';
|
||||
import { Sqlite } from '../services/sqlite';
|
||||
import { Establishment } from '../models/Establishment';
|
||||
import { Chain } from '../models/Chain';
|
||||
import { EstablishmentQueryResult } from '../types/sqlite.type';
|
||||
|
||||
describe('EstablishmentDAO', () => {
|
||||
let service: EstablishmentDAO;
|
||||
let sqlite: Partial<Sqlite>;
|
||||
let ESTABLISHMENT_MOCK: Establishment;
|
||||
let QUERY_RESULT_MOCK: EstablishmentQueryResult;
|
||||
|
||||
beforeEach(() => {
|
||||
QUERY_RESULT_MOCK = {
|
||||
address: 'mock street',
|
||||
image: 'mock.jpg',
|
||||
name: 'mock',
|
||||
chain_id: 1,
|
||||
id: 1,
|
||||
};
|
||||
ESTABLISHMENT_MOCK = new Establishment(
|
||||
new Chain(QUERY_RESULT_MOCK.name, QUERY_RESULT_MOCK.image, QUERY_RESULT_MOCK.chain_id),
|
||||
QUERY_RESULT_MOCK.address,
|
||||
QUERY_RESULT_MOCK.id,
|
||||
);
|
||||
|
||||
sqlite = {
|
||||
executeQuery: vi.fn().mockResolvedValue({ rows: [QUERY_RESULT_MOCK] }),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{ provide: Sqlite, useValue: sqlite }],
|
||||
});
|
||||
|
||||
service = TestBed.inject(EstablishmentDAO);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should receive mapped results on findAll', async () => {
|
||||
const result = await service.findAll();
|
||||
if (result.length > 0) {
|
||||
expect(result[0]).toEqual(ESTABLISHMENT_MOCK);
|
||||
} else {
|
||||
test.fails('Expected mock response to contain at least one value');
|
||||
}
|
||||
});
|
||||
|
||||
it('should receive mapped results on findBy', async () => {
|
||||
const result = await service.findBy({ id: 1 });
|
||||
if (result.length > 0) {
|
||||
expect(result[0]).toEqual(ESTABLISHMENT_MOCK);
|
||||
} else {
|
||||
test.fails('Expected mock response to contain at least one value');
|
||||
}
|
||||
});
|
||||
|
||||
it('should call executeQuery with object fields on insert', async () => {
|
||||
sqlite.executeQuery = vi.fn().mockResolvedValue({ rows: [] });
|
||||
const expectedSql = 'INSERT INTO establishment ( address, chain_id ) VALUES ( ?, ? );';
|
||||
const expectedParams = [ESTABLISHMENT_MOCK.address, ESTABLISHMENT_MOCK.chain.id];
|
||||
ESTABLISHMENT_MOCK.id = undefined;
|
||||
await service.insert(ESTABLISHMENT_MOCK);
|
||||
expect(sqlite.executeQuery).toHaveBeenCalledExactlyOnceWith(expectedSql, expectedParams);
|
||||
});
|
||||
|
||||
it('should throw if toDB is called with a chain that has no id', async () => {
|
||||
sqlite.executeQuery = vi.fn().mockResolvedValue({ rows: [] });
|
||||
ESTABLISHMENT_MOCK.id = undefined;
|
||||
ESTABLISHMENT_MOCK.chain.id = undefined;
|
||||
expect(service.insert(ESTABLISHMENT_MOCK)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
33
src/app/dao/EstablishmentDAO.ts
Normal file
33
src/app/dao/EstablishmentDAO.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Establishment } from '../models/Establishment';
|
||||
import { Chain } from '../models/Chain';
|
||||
import { DBEstablishment } from '../models/db/DBEstablishment';
|
||||
import { ComposedDAO } from './ComposedDAO';
|
||||
import { EstablishmentQueryResult } from '../types/sqlite.type';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class EstablishmentDAO extends ComposedDAO<
|
||||
Establishment,
|
||||
DBEstablishment,
|
||||
EstablishmentQueryResult
|
||||
> {
|
||||
constructor() {
|
||||
super(
|
||||
Establishment.name,
|
||||
`SELECT e.id, e.address, chain_id, c.name, c.image FROM establishment e JOIN chain c ON c.id = chain_id;`,
|
||||
'e',
|
||||
);
|
||||
}
|
||||
|
||||
protected override toDB(model: Establishment): DBEstablishment {
|
||||
if (!model.chain.id) throw new Error('Chain id is required');
|
||||
return new DBEstablishment(model.address, model.chain.id, model.id);
|
||||
}
|
||||
|
||||
protected override fromDB(qR: EstablishmentQueryResult): Establishment {
|
||||
const chain = new Chain(qR.name, qR.image, qR.chain_id);
|
||||
return new Establishment(chain, qR.address, qR.id);
|
||||
}
|
||||
}
|
||||
46
src/app/dao/ProductDAO.spec.ts
Normal file
46
src/app/dao/ProductDAO.spec.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ProductDAO } from './ProductDAO';
|
||||
import { Sqlite } from '../services/sqlite';
|
||||
import { Product } from '../models/Product';
|
||||
|
||||
describe('ProductDAO', () => {
|
||||
let service: ProductDAO;
|
||||
let sqlite: Partial<Sqlite>;
|
||||
|
||||
let PRODUCT_MOCK: Product;
|
||||
|
||||
beforeEach(() => {
|
||||
PRODUCT_MOCK = new Product('12121112', 'mock', 'mock.jpg', 1);
|
||||
|
||||
sqlite = {
|
||||
executeQuery: vi.fn().mockResolvedValue({ rows: [PRODUCT_MOCK] }),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{ provide: Sqlite, useValue: sqlite }],
|
||||
});
|
||||
|
||||
service = TestBed.inject(ProductDAO);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should receive mapped results on findAll', async () => {
|
||||
const result = await service.findAll();
|
||||
if (result.length > 0) {
|
||||
expect(result[0]).toEqual(PRODUCT_MOCK);
|
||||
} else {
|
||||
test.fails('Expected mock response to contain at least one value');
|
||||
}
|
||||
});
|
||||
|
||||
it('should call executeQuery with object fields on insert', async () => {
|
||||
PRODUCT_MOCK.id = undefined;
|
||||
const expectedSql = 'INSERT INTO product ( barcode, name, image ) VALUES ( ?, ?, ? );';
|
||||
const expectedParams: any[] = [PRODUCT_MOCK.barcode, PRODUCT_MOCK.name, PRODUCT_MOCK.image];
|
||||
await service.insert(PRODUCT_MOCK);
|
||||
expect(sqlite.executeQuery).toHaveBeenCalledExactlyOnceWith(expectedSql, expectedParams);
|
||||
});
|
||||
});
|
||||
21
src/app/dao/ProductDAO.ts
Normal file
21
src/app/dao/ProductDAO.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Product } from '../models/Product';
|
||||
import { BaseDAO } from './BaseDAO';
|
||||
import { DBProduct } from '../models/db/DBProduct';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ProductDAO extends BaseDAO<Product, DBProduct, DBProduct> {
|
||||
constructor() {
|
||||
super(Product.name);
|
||||
}
|
||||
|
||||
protected override toDB(model: Product): DBProduct {
|
||||
return new Product(model.barcode, model.name, model.image, model.id);
|
||||
}
|
||||
|
||||
protected override fromDB(qR: DBProduct): Product {
|
||||
return new DBProduct(qR.barcode, qR.name, qR.image, qR.id);
|
||||
}
|
||||
}
|
||||
86
src/app/dao/ProductEstablishmentDAO.spec.ts
Normal file
86
src/app/dao/ProductEstablishmentDAO.spec.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ProductEstablishmentDAO } from './ProductEstablishmentDAO';
|
||||
import { Sqlite } from '../services/sqlite';
|
||||
import { ProductEstablishment } from '../models/ProductEstablisment';
|
||||
import { Product } from '../models/Product';
|
||||
import { Establishment } from '../models/Establishment';
|
||||
import { Chain } from '../models/Chain';
|
||||
import { ProductEstablishmentQueryResult } from '../types/sqlite.type';
|
||||
|
||||
describe('ProductEstablishmentDAO', () => {
|
||||
let service: ProductEstablishmentDAO;
|
||||
let sqlite: Partial<Sqlite>;
|
||||
|
||||
let PRODUCT_ESTABLISHMENT_MOCK: ProductEstablishment;
|
||||
let PRODUCT_ESTABLISHMENT_QUERY_RESULT: ProductEstablishmentQueryResult;
|
||||
|
||||
beforeEach(() => {
|
||||
PRODUCT_ESTABLISHMENT_MOCK = new ProductEstablishment(
|
||||
new Product('121212', 'mock', 'mock.jpg', 1),
|
||||
new Establishment(new Chain('mock', 'mock.png', 1), 'mock street', 1),
|
||||
1,
|
||||
);
|
||||
PRODUCT_ESTABLISHMENT_QUERY_RESULT = {
|
||||
address: PRODUCT_ESTABLISHMENT_MOCK.establishment.address,
|
||||
barcode: PRODUCT_ESTABLISHMENT_MOCK.product.barcode,
|
||||
chain_id: PRODUCT_ESTABLISHMENT_MOCK.establishment.chain.id!,
|
||||
chain_image: PRODUCT_ESTABLISHMENT_MOCK.establishment.chain.image,
|
||||
chain_name: PRODUCT_ESTABLISHMENT_MOCK.establishment.chain.name,
|
||||
establishment_id: PRODUCT_ESTABLISHMENT_MOCK.establishment.id!,
|
||||
id: PRODUCT_ESTABLISHMENT_MOCK.id!,
|
||||
product_id: PRODUCT_ESTABLISHMENT_MOCK.product.id!,
|
||||
product_image: PRODUCT_ESTABLISHMENT_MOCK.product.image,
|
||||
product_name: PRODUCT_ESTABLISHMENT_MOCK.product.name,
|
||||
};
|
||||
|
||||
sqlite = {
|
||||
executeQuery: vi.fn().mockResolvedValue({ rows: [PRODUCT_ESTABLISHMENT_QUERY_RESULT] }),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{ provide: Sqlite, useValue: sqlite }],
|
||||
});
|
||||
|
||||
service = TestBed.inject(ProductEstablishmentDAO);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(PRODUCT_ESTABLISHMENT_MOCK).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should receive mapped results on findAll', async () => {
|
||||
const result = await service.findAll();
|
||||
if (result.length > 0) {
|
||||
expect(result[0]).toEqual(PRODUCT_ESTABLISHMENT_MOCK);
|
||||
} else {
|
||||
test.fails('Expected mock response to contain at least one value');
|
||||
}
|
||||
});
|
||||
|
||||
it('should call executeQuery with object fields on insert', async () => {
|
||||
sqlite.executeQuery = vi.fn().mockResolvedValue({ rows: [] });
|
||||
const expectedSql =
|
||||
'INSERT INTO product_establishment ( product_id, establishment_id ) VALUES ( ?, ? );';
|
||||
const expectedParams: any[] = [
|
||||
PRODUCT_ESTABLISHMENT_MOCK.product.id,
|
||||
PRODUCT_ESTABLISHMENT_MOCK.establishment.id,
|
||||
];
|
||||
PRODUCT_ESTABLISHMENT_MOCK.id = undefined;
|
||||
await service.insert(PRODUCT_ESTABLISHMENT_MOCK);
|
||||
expect(sqlite.executeQuery).toHaveBeenCalledExactlyOnceWith(expectedSql, expectedParams);
|
||||
});
|
||||
|
||||
describe('toDB', () => {
|
||||
it('should throw if toDB is called with a product that has no id', async () => {
|
||||
sqlite.executeQuery = vi.fn().mockResolvedValue({ rows: [] });
|
||||
PRODUCT_ESTABLISHMENT_MOCK.product.id = undefined;
|
||||
expect(service.insert(PRODUCT_ESTABLISHMENT_MOCK)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should throw if toDB is called with a establishmen that has no id', async () => {
|
||||
sqlite.executeQuery = vi.fn().mockResolvedValue({ rows: [] });
|
||||
PRODUCT_ESTABLISHMENT_MOCK.establishment.id = undefined;
|
||||
expect(service.insert(PRODUCT_ESTABLISHMENT_MOCK)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
43
src/app/dao/ProductEstablishmentDAO.ts
Normal file
43
src/app/dao/ProductEstablishmentDAO.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Establishment } from '../models/Establishment';
|
||||
import { Product } from '../models/Product';
|
||||
import { ProductEstablishment } from '../models/ProductEstablisment';
|
||||
import { Chain } from '../models/Chain';
|
||||
import { DBProductEstablishment } from '../models/db/DBProductEstablishment';
|
||||
import { ComposedDAO } from './ComposedDAO';
|
||||
import { ProductEstablishmentQueryResult } from '../types/sqlite.type';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ProductEstablishmentDAO extends ComposedDAO<
|
||||
ProductEstablishment,
|
||||
DBProductEstablishment,
|
||||
ProductEstablishmentQueryResult
|
||||
> {
|
||||
constructor() {
|
||||
super(
|
||||
'product_establishment',
|
||||
`
|
||||
SELECT pe.id, pe.product_id, pe.establishment_id, p.barcode, p.image as product_image, p.name as product_name,
|
||||
e.address, e.chain_id, c.image as chain_image, c.name as chain_name FROM product_establishment pe
|
||||
JOIN product p ON p.id = pe.product_id
|
||||
JOIN establishment e ON e.id = pe.establishment_id
|
||||
JOIN chain c ON c.id = e.chain_id;`,
|
||||
'pe',
|
||||
);
|
||||
}
|
||||
|
||||
protected override toDB(model: ProductEstablishment): DBProductEstablishment {
|
||||
if (!model.product.id) throw new Error('Product id is required');
|
||||
if (!model.establishment.id) throw new Error('Establishment id is required');
|
||||
return new DBProductEstablishment(model.product.id, model.establishment.id, model.id);
|
||||
}
|
||||
|
||||
protected override fromDB(qR: ProductEstablishmentQueryResult): ProductEstablishment {
|
||||
const chain = new Chain(qR.chain_name, qR.chain_image, qR.chain_id);
|
||||
const establishment = new Establishment(chain, qR.address, qR.establishment_id);
|
||||
const product = new Product(qR.barcode, qR.product_name, qR.product_image, qR.product_id);
|
||||
return new ProductEstablishment(product, establishment, qR.id);
|
||||
}
|
||||
}
|
||||
95
src/app/dao/PurchaseDAO.spec.ts
Normal file
95
src/app/dao/PurchaseDAO.spec.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { PurchaseDAO } from './PurchaseDAO';
|
||||
import { Sqlite } from '../services/sqlite';
|
||||
import { Purchase } from '../models/Purchase';
|
||||
import { Establishment } from '../models/Establishment';
|
||||
import { Chain } from '../models/Chain';
|
||||
import { Product } from '../models/Product';
|
||||
import { PurchaseQueryResult } from '../types/sqlite.type';
|
||||
|
||||
describe('PurchaseDAO', () => {
|
||||
let service: PurchaseDAO;
|
||||
let sqlite: Partial<Sqlite>;
|
||||
|
||||
let PURCHASE_MOCK: Purchase;
|
||||
let PURCHASE_QUERY_RESULT_MOCK: PurchaseQueryResult;
|
||||
|
||||
beforeEach(() => {
|
||||
PURCHASE_MOCK = new Purchase(
|
||||
new Establishment(new Chain('mock', 'mock.jpg', 1), 'mock street', 1),
|
||||
new Product('1212112', 'mock', 'mock.png', 1),
|
||||
1245.55,
|
||||
3,
|
||||
Date.now(),
|
||||
1,
|
||||
);
|
||||
PURCHASE_QUERY_RESULT_MOCK = {
|
||||
address: PURCHASE_MOCK.establishment.address,
|
||||
barcode: PURCHASE_MOCK.product.barcode,
|
||||
chain_id: PURCHASE_MOCK.establishment.chain.id!,
|
||||
chain_image: PURCHASE_MOCK.establishment.chain.image,
|
||||
chain_name: PURCHASE_MOCK.establishment.chain.name,
|
||||
date: PURCHASE_MOCK.date,
|
||||
establishment_id: PURCHASE_MOCK.establishment.id!,
|
||||
id: PURCHASE_MOCK.id!,
|
||||
price: PURCHASE_MOCK.price,
|
||||
product_id: PURCHASE_MOCK.product.id!,
|
||||
product_image: PURCHASE_MOCK.product.image,
|
||||
product_name: PURCHASE_MOCK.product.name,
|
||||
quantity: PURCHASE_MOCK.quantity,
|
||||
};
|
||||
|
||||
sqlite = {
|
||||
executeQuery: vi.fn().mockResolvedValue({ rows: [PURCHASE_QUERY_RESULT_MOCK] }),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{ provide: Sqlite, useValue: sqlite }],
|
||||
});
|
||||
|
||||
service = TestBed.inject(PurchaseDAO);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(PURCHASE_MOCK).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should receive mapped results on findAll', async () => {
|
||||
const result = await service.findAll();
|
||||
if (result.length > 0) {
|
||||
expect(result[0]).toEqual(PURCHASE_MOCK);
|
||||
} else {
|
||||
test.fails('Expected mock response to contain at least one value');
|
||||
}
|
||||
});
|
||||
|
||||
it('should call executeQuery with object fields on insert', async () => {
|
||||
sqlite.executeQuery = vi.fn().mockResolvedValue({ rows: [] });
|
||||
const expectedSql =
|
||||
'INSERT INTO purchase ( establishment_id, product_id, date, price, quantity ) VALUES ( ?, ?, ?, ?, ? );';
|
||||
const expectedParams: any[] = [
|
||||
PURCHASE_MOCK.establishment.id,
|
||||
PURCHASE_MOCK.product.id,
|
||||
PURCHASE_MOCK.date,
|
||||
PURCHASE_MOCK.price * 100,
|
||||
PURCHASE_MOCK.quantity,
|
||||
];
|
||||
PURCHASE_MOCK.id = undefined;
|
||||
await service.insert(PURCHASE_MOCK);
|
||||
expect(sqlite.executeQuery).toHaveBeenCalledExactlyOnceWith(expectedSql, expectedParams);
|
||||
});
|
||||
|
||||
describe('toDB', () => {
|
||||
it('should throw if toDB is called with a establishmen that has no id', async () => {
|
||||
sqlite.executeQuery = vi.fn().mockResolvedValue({ rows: [] });
|
||||
PURCHASE_MOCK.establishment.id = undefined;
|
||||
expect(service.insert(PURCHASE_MOCK)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should throw if toDB is called with a product that has no id', async () => {
|
||||
sqlite.executeQuery = vi.fn().mockResolvedValue({ rows: [] });
|
||||
PURCHASE_MOCK.product.id = undefined;
|
||||
expect(service.insert(PURCHASE_MOCK)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
48
src/app/dao/PurchaseDAO.ts
Normal file
48
src/app/dao/PurchaseDAO.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Purchase } from '../models/Purchase';
|
||||
import { DBPurchase } from '../models/db/DBPurchase';
|
||||
import { Establishment } from '../models/Establishment';
|
||||
import { Chain } from '../models/Chain';
|
||||
import { Product } from '../models/Product';
|
||||
import { ComposedDAO } from './ComposedDAO';
|
||||
import { PurchaseQueryResult } from '../types/sqlite.type';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PurchaseDAO extends ComposedDAO<Purchase, DBPurchase, PurchaseQueryResult> {
|
||||
constructor() {
|
||||
super(
|
||||
Purchase.name,
|
||||
`SELECT p.id, p.price, p.quantity, p.date, p.establishment_id, p.product_id,
|
||||
pr.barcode, pr.image as product_image, pr.name as product_name,
|
||||
e.address, e.chain_id,
|
||||
c.image as chain_image, c.name as chain_name
|
||||
FROM purchase p
|
||||
JOIN product pr ON pr.id = p.product_id
|
||||
JOIN establishment e ON e.id = p.establishment_id
|
||||
JOIN chain c ON c.id = e.chain_id;`,
|
||||
'e',
|
||||
);
|
||||
}
|
||||
|
||||
protected override toDB(model: Purchase): DBPurchase {
|
||||
if (!model.establishment.id) throw new Error('Esablishment id is required');
|
||||
if (!model.product.id) throw new Error('Product id is required');
|
||||
return new DBPurchase(
|
||||
model.establishment.id,
|
||||
model.product.id,
|
||||
model.date,
|
||||
model.price * 100,
|
||||
model.quantity,
|
||||
model.id,
|
||||
);
|
||||
}
|
||||
|
||||
protected override fromDB(qR: PurchaseQueryResult): Purchase {
|
||||
const chain = new Chain(qR.chain_name, qR.chain_image, qR.chain_id);
|
||||
const establishment = new Establishment(chain, qR.address, qR.establishment_id);
|
||||
const product = new Product(qR.barcode, qR.product_name, qR.product_image, qR.product_id);
|
||||
return new Purchase(establishment, product, qR.price / 100, qR.quantity, qR.date, qR.id);
|
||||
}
|
||||
}
|
||||
7
src/app/models/Chain.spec.ts
Normal file
7
src/app/models/Chain.spec.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Chain } from './Chain';
|
||||
|
||||
describe('Chain', () => {
|
||||
it('should create', () => {
|
||||
expect(new Chain('mock', 'mock.jpg', 1)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
3
src/app/models/Chain.ts
Normal file
3
src/app/models/Chain.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export class Chain {
|
||||
constructor(public name: string | null, public image: string | null, public id?: number) {}
|
||||
}
|
||||
8
src/app/models/Establishment.spec.ts
Normal file
8
src/app/models/Establishment.spec.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Chain } from './Chain';
|
||||
import { Establishment } from './Establishment';
|
||||
|
||||
describe('Establishment', () => {
|
||||
it('should create', () => {
|
||||
expect(new Establishment(new Chain('mock', 'mock.jpg', 1), 'Mock street', 1)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
5
src/app/models/Establishment.ts
Normal file
5
src/app/models/Establishment.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Chain } from './Chain';
|
||||
|
||||
export class Establishment {
|
||||
constructor(public chain: Chain, public address = '', public id?: number) {}
|
||||
}
|
||||
7
src/app/models/Product.spec.ts
Normal file
7
src/app/models/Product.spec.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Product } from './Product';
|
||||
|
||||
describe('Product', () => {
|
||||
it('should create', () => {
|
||||
expect(new Product('1212121121', 'Mock Product', 'product.png', 1)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
8
src/app/models/Product.ts
Normal file
8
src/app/models/Product.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export class Product {
|
||||
constructor(
|
||||
public barcode: string,
|
||||
public name = '',
|
||||
public image: string | null = null,
|
||||
public id?: number
|
||||
) {}
|
||||
}
|
||||
16
src/app/models/ProductEstablisment.spec.ts
Normal file
16
src/app/models/ProductEstablisment.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Chain } from './Chain';
|
||||
import { Establishment } from './Establishment';
|
||||
import { Product } from './Product';
|
||||
import { ProductEstablishment } from './ProductEstablisment';
|
||||
|
||||
describe('ProductEstablishment', () => {
|
||||
it('should create', () => {
|
||||
expect(
|
||||
new ProductEstablishment(
|
||||
new Product('12121212', 'mock product', 'mock.jpg', 1),
|
||||
new Establishment(new Chain('mock', 'mock', 1)),
|
||||
1,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
14
src/app/models/ProductEstablisment.ts
Normal file
14
src/app/models/ProductEstablisment.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Establishment } from './Establishment';
|
||||
import { Product } from './Product';
|
||||
|
||||
export class ProductEstablishment {
|
||||
id?: number;
|
||||
establishment: Establishment;
|
||||
product: Product;
|
||||
|
||||
constructor(product: Product, establishment: Establishment, id?: number) {
|
||||
this.id = id;
|
||||
this.establishment = establishment;
|
||||
this.product = product;
|
||||
}
|
||||
}
|
||||
25
src/app/models/Purchase.spec.ts
Normal file
25
src/app/models/Purchase.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Chain } from './Chain';
|
||||
import { Establishment } from './Establishment';
|
||||
import { Product } from './Product';
|
||||
import { Purchase } from './Purchase';
|
||||
|
||||
describe('Purchase', () => {
|
||||
let purchase: Purchase;
|
||||
beforeEach(() => {
|
||||
purchase = new Purchase(
|
||||
new Establishment(new Chain('mock', 'image_mock.png', 1), 'mock street', 1),
|
||||
new Product('121212121221', 'mock product', 'image_mock.png', 1),
|
||||
1254.51,
|
||||
)
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(purchase).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should divide by 100 on price get and multiply by 100 on set', () => {
|
||||
const PRICE_MOCK = 1475.56;
|
||||
purchase.price = PRICE_MOCK;
|
||||
expect(purchase.price).toBe(PRICE_MOCK);
|
||||
});
|
||||
});
|
||||
24
src/app/models/Purchase.ts
Normal file
24
src/app/models/Purchase.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Establishment } from './Establishment';
|
||||
import { Product } from './Product';
|
||||
|
||||
export class Purchase {
|
||||
#price: number;
|
||||
constructor(
|
||||
public establishment: Establishment,
|
||||
public product: Product,
|
||||
price = 0,
|
||||
public quantity = 1,
|
||||
public date = Date.now(),
|
||||
public id?: number
|
||||
) {
|
||||
this.#price = price * 100;
|
||||
}
|
||||
|
||||
get price() {
|
||||
return this.#price / 100;
|
||||
}
|
||||
|
||||
set price(price: number) {
|
||||
this.#price = price * 100;
|
||||
}
|
||||
}
|
||||
7
src/app/models/db/DBChain.spec.ts
Normal file
7
src/app/models/db/DBChain.spec.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { DBChain } from './DBChain';
|
||||
|
||||
describe('Chain', () => {
|
||||
it('should create', () => {
|
||||
expect(new DBChain('mock', 'mock.jpg', 1)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
3
src/app/models/db/DBChain.ts
Normal file
3
src/app/models/db/DBChain.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Chain } from '../Chain';
|
||||
|
||||
export class DBChain extends Chain {}
|
||||
7
src/app/models/db/DBEstablishment.spec.ts
Normal file
7
src/app/models/db/DBEstablishment.spec.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { DBEstablishment } from './DBEstablishment';
|
||||
|
||||
describe('DBEstablishment', () => {
|
||||
it('should create', () => {
|
||||
expect(new DBEstablishment('mock address', 1, 1)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
3
src/app/models/db/DBEstablishment.ts
Normal file
3
src/app/models/db/DBEstablishment.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export class DBEstablishment {
|
||||
constructor(public address: string, public chain_id: number, public id?: number) {}
|
||||
}
|
||||
7
src/app/models/db/DBProduct.spec.ts
Normal file
7
src/app/models/db/DBProduct.spec.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { DBProduct } from './DBProduct';
|
||||
|
||||
describe('DBProduct', () => {
|
||||
it('should create', () => {
|
||||
expect(new DBProduct('121212112', 'product mock', 'mock.jpg', 1)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
3
src/app/models/db/DBProduct.ts
Normal file
3
src/app/models/db/DBProduct.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Product } from '../Product';
|
||||
|
||||
export class DBProduct extends Product {}
|
||||
7
src/app/models/db/DBProductEstablishment.spec.ts
Normal file
7
src/app/models/db/DBProductEstablishment.spec.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { DBProductEstablishment } from './DBProductEstablishment';
|
||||
|
||||
describe('DBProductEstablishment', () => {
|
||||
it('should create', () => {
|
||||
expect(new DBProductEstablishment(1, 1, 1)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
3
src/app/models/db/DBProductEstablishment.ts
Normal file
3
src/app/models/db/DBProductEstablishment.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export class DBProductEstablishment {
|
||||
constructor(public product_id: number, public establishment_id: number, public id?: number) {}
|
||||
}
|
||||
7
src/app/models/db/DBPurchase.spec.ts
Normal file
7
src/app/models/db/DBPurchase.spec.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { DBPurchase } from './DBPurchase';
|
||||
|
||||
describe('DBPurchase', () => {
|
||||
it('should create', () => {
|
||||
expect(new DBPurchase(1, 1, Date.now(), 1000, 1, 1)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
10
src/app/models/db/DBPurchase.ts
Normal file
10
src/app/models/db/DBPurchase.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export class DBPurchase {
|
||||
constructor(
|
||||
public establishment_id: number,
|
||||
public product_id: number,
|
||||
public date: number,
|
||||
public price = 0,
|
||||
public quantity = 1,
|
||||
public id?: number
|
||||
) {}
|
||||
}
|
||||
67
src/app/services/sqlite.spec.ts
Normal file
67
src/app/services/sqlite.spec.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { vi } from 'vitest';
|
||||
import { Sqlite } from './sqlite';
|
||||
import { WebSqlite } from 'angular-web-sqlite';
|
||||
import { BatchOp } from '../types/sqlite.type';
|
||||
|
||||
describe('Sqlite', () => {
|
||||
let service: Sqlite;
|
||||
let webSqlite: Partial<WebSqlite>;
|
||||
|
||||
beforeEach(() => {
|
||||
webSqlite = {
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
executeSql: vi.fn().mockResolvedValue({ rows: [] }),
|
||||
batchSql: vi.fn().mockResolvedValue({ rows: [] }),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{ provide: WebSqlite, useValue: webSqlite }],
|
||||
});
|
||||
service = TestBed.inject(Sqlite);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should init database', () => {
|
||||
service.initializeDatabase('test').then(() => expect(webSqlite.init).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
it('should call executeSql with the given parameters', () => {
|
||||
const SQL_MOCK = `INSERT INTO MOCK VALUES ( ?, ? );`;
|
||||
const PARAMS_MOCK = [1, 'MOCK'];
|
||||
service
|
||||
.executeQuery(SQL_MOCK, PARAMS_MOCK)
|
||||
.then(() =>
|
||||
expect(webSqlite.executeSql).toHaveBeenCalledExactlyOnceWith(SQL_MOCK, PARAMS_MOCK),
|
||||
);
|
||||
});
|
||||
|
||||
it('should call batchSql with the given parameters', () => {
|
||||
const batch: BatchOp[] = [
|
||||
['DROP TABLE chain;', []],
|
||||
['DROP TABLE establishment;', []],
|
||||
];
|
||||
service
|
||||
.batchSqlOperations(batch)
|
||||
.then(() => expect(webSqlite.batchSql).toHaveBeenCalledExactlyOnceWith(batch));
|
||||
});
|
||||
|
||||
it('should drop all existing tables that are not sqlite engine related', () => {
|
||||
const batchSqlOperationsSpy = vi
|
||||
.spyOn(service, 'batchSqlOperations')
|
||||
.mockResolvedValue({ rows: [] });
|
||||
const TABLES_QUERY_RESULT_MOCK = {
|
||||
rows: [{ name: 'chain' }, { name: 'establishment' }, { name: 'sqlite_sequence' }],
|
||||
};
|
||||
const expectedBatch = TABLES_QUERY_RESULT_MOCK.rows
|
||||
.filter((r) => !r.name.startsWith('sqlite_'))
|
||||
.map((table) => [`DROP TABLE ${table.name};`, []]);
|
||||
webSqlite.executeSql = vi.fn().mockResolvedValue(TABLES_QUERY_RESULT_MOCK);
|
||||
service
|
||||
.dropAllTables()
|
||||
.then(() => expect(batchSqlOperationsSpy).toHaveBeenCalledExactlyOnceWith(expectedBatch));
|
||||
});
|
||||
});
|
||||
34
src/app/services/sqlite.ts
Normal file
34
src/app/services/sqlite.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { WebSqlite } from 'angular-web-sqlite';
|
||||
import { BatchOp } from '../types/sqlite.type';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class Sqlite {
|
||||
private readonly webSqlite = inject(WebSqlite);
|
||||
|
||||
initializeDatabase(dbName: string) {
|
||||
return this.webSqlite.init(dbName);
|
||||
}
|
||||
|
||||
async executeQuery(sql: string, params: any[] = []) {
|
||||
const result = await this.webSqlite.executeSql(sql, params);
|
||||
return result;
|
||||
}
|
||||
|
||||
async batchSqlOperations(ops: BatchOp[]) {
|
||||
const result = await this.webSqlite.batchSql(ops);
|
||||
return result;
|
||||
}
|
||||
|
||||
async dropAllTables() {
|
||||
const tables: { rows: { name: string }[] } = await this.executeQuery(
|
||||
"SELECT * FROM sqlite_master WHERE type='table';"
|
||||
);
|
||||
const dropQueries: BatchOp[] = tables.rows
|
||||
.filter((r) => !r.name.startsWith('sqlite_'))
|
||||
.map((table) => [`DROP TABLE ${table.name};`, []]);
|
||||
return this.batchSqlOperations(dropQueries);
|
||||
}
|
||||
}
|
||||
22
src/app/types/globalThis.d.ts
vendored
Normal file
22
src/app/types/globalThis.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
type ImageBitmapSource = HTMLImageElement | SVGImageElement | HTMLVideoElement | HTMLCanvasElement | ImageBitmap | OffscreenCanvas | VideoFrame | Blob | ImageData
|
||||
|
||||
export type BarcodeDetectorOptions = {
|
||||
formats: string[]
|
||||
}
|
||||
|
||||
export type DetectedBarcode = {
|
||||
boundingBox: DOMRectReadOnly;
|
||||
cornerPoints: coords[];
|
||||
format: string[];
|
||||
rawValue: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
var BarcodeDetector: BarcodeDetector;
|
||||
}
|
||||
|
||||
declare type BarcodeDetector = {
|
||||
new (formats?: {formats: string[]}): BarcodeDetector;
|
||||
static getSupportedFormats(): Promise<string[]>;
|
||||
detect(imageBitmapSource: ImageBitmapSource): Promise<DetectedBarcode[]>;
|
||||
}
|
||||
37
src/app/types/sqlite.type.ts
Normal file
37
src/app/types/sqlite.type.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Chain } from '../models/Chain';
|
||||
import { Establishment } from '../models/Establishment';
|
||||
|
||||
export type QueryResult<T> = { rows: T[] };
|
||||
export type BatchOp = [string, any[]];
|
||||
|
||||
export type PurchaseQueryResult = {
|
||||
id: number;
|
||||
price: number;
|
||||
quantity: number;
|
||||
date: number;
|
||||
establishment_id: number;
|
||||
product_id: number;
|
||||
barcode: string;
|
||||
product_image: string | null;
|
||||
product_name: string;
|
||||
address: string;
|
||||
chain_id: number;
|
||||
chain_image: string | null;
|
||||
chain_name: string | null;
|
||||
};
|
||||
|
||||
export type ProductEstablishmentQueryResult = {
|
||||
address: string;
|
||||
barcode: string;
|
||||
chain_id: number;
|
||||
chain_image: string | null;
|
||||
chain_name: string | null;
|
||||
establishment_id: number;
|
||||
id: number;
|
||||
product_id: number;
|
||||
product_image: string | null;
|
||||
product_name: string;
|
||||
};
|
||||
|
||||
export type EstablishmentQueryResult = Omit<Establishment, 'chain'> &
|
||||
Omit<Chain, 'id'> & { id: number; chain_id: number };
|
||||
198
src/app/web-components/barcode-reader.ts
Normal file
198
src/app/web-components/barcode-reader.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import type { BarcodeDetector, DetectedBarcode } from '../types/globalThis';
|
||||
|
||||
export class BarcodeReader extends HTMLElement {
|
||||
animationFrameId = null;
|
||||
constraints = {
|
||||
audio: false,
|
||||
video: {
|
||||
facingMode: 'environment',
|
||||
},
|
||||
};
|
||||
container = document.createElement('div');
|
||||
video = document.createElement('video');
|
||||
dataCanvas = document.createElement('canvas');
|
||||
dataCtx: CanvasRenderingContext2D | null | undefined;
|
||||
detector: BarcodeDetector | null = null;
|
||||
code: DetectedBarcode | null = null;
|
||||
stream: MediaStream | null = null;
|
||||
scanId = -1;
|
||||
frame = document.createElement('div');
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = '/barcode-reader/barcode-reader.css';
|
||||
this.container.setAttribute('id', 'container');
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.textContent = 'X';
|
||||
closeBtn.onclick = this.stopScan.bind(this);
|
||||
closeBtn.classList.add('close-btn')
|
||||
this.container.appendChild(closeBtn);
|
||||
const videoContainer = document.createElement('div');
|
||||
videoContainer.setAttribute('id', 'video-container');
|
||||
this.container.appendChild(videoContainer);
|
||||
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.classList.add('overlay');
|
||||
this.video.setAttribute('autoplay', '');
|
||||
this.video.setAttribute('playsinline', '');
|
||||
this.dataCanvas.setAttribute('id', 'data-canvas');
|
||||
|
||||
this.frame.setAttribute('id', 'frame');
|
||||
this.frame.style.display = 'none';
|
||||
for (let el of ['top-left', 'top-right', 'bottom-right', 'bottom-left']) {
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('corner', el);
|
||||
this.frame.appendChild(div);
|
||||
}
|
||||
videoContainer.appendChild(overlay);
|
||||
videoContainer.appendChild(this.dataCanvas);
|
||||
videoContainer.appendChild(this.video);
|
||||
videoContainer.appendChild(this.frame);
|
||||
|
||||
const shadowRoot = this.attachShadow({ mode: 'open' });
|
||||
shadowRoot.appendChild(link);
|
||||
shadowRoot.appendChild(this.container);
|
||||
this.capturing = false;
|
||||
|
||||
this.dataCtx = this.dataCanvas.getContext('2d', { willReadFrequently: true, alpha: false });
|
||||
}
|
||||
|
||||
get capturing() {
|
||||
return this.hasAttribute('capturing');
|
||||
}
|
||||
|
||||
set capturing(isCapturing) {
|
||||
if (isCapturing) {
|
||||
this.setAttribute('capturing', '');
|
||||
} else {
|
||||
this.removeAttribute('capturing');
|
||||
}
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
this.detector = await this.getDetector();
|
||||
this.video.onplaying = async () => {
|
||||
if (this.dataCanvas) {
|
||||
this.dataCanvas.width = this.dataCanvas.offsetWidth;
|
||||
this.dataCanvas.height = this.dataCanvas.offsetHeight;
|
||||
this.getResults();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async getDetector(): Promise<BarcodeDetector | null> {
|
||||
if (globalThis.BarcodeDetector) {
|
||||
const formats = await BarcodeDetector.getSupportedFormats();
|
||||
return new BarcodeDetector({ formats });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async scan() {
|
||||
this.showContainer(true);
|
||||
|
||||
try {
|
||||
this.capturing = true;
|
||||
this.code = null;
|
||||
|
||||
if (this.stream instanceof MediaStream) {
|
||||
await this.getResults();
|
||||
} else {
|
||||
this.dispatchEvent(new CustomEvent('scan-status', { detail: { state: 'loading' } }));
|
||||
this.stream = await navigator.mediaDevices.getUserMedia(this.constraints);
|
||||
this.startVideo();
|
||||
this.showFrame();
|
||||
this.dispatchEvent(new CustomEvent('scan-status', { detail: { state: 'started' } }));
|
||||
this.dispatchEvent(new CustomEvent('scan-start'));
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
this.showContainer(false);
|
||||
this.dispatchEvent(new CustomEvent('scan-status', { detail: { state: 'stopped' } }));
|
||||
this.dispatchEvent(new CustomEvent('scan-permission-denied'));
|
||||
this.capturing = false;
|
||||
}
|
||||
}
|
||||
|
||||
stopScan() {
|
||||
clearTimeout(this.scanId);
|
||||
if (this.stream) {
|
||||
this.capturing = false;
|
||||
this.stream.getTracks().forEach((track) => track.stop());
|
||||
this.stream = null;
|
||||
this.video.srcObject = null;
|
||||
this.dispatchEvent(new CustomEvent('scan-stop'));
|
||||
this.dispatchEvent(new CustomEvent('scan-status', { detail: { state: 'stopped' } }));
|
||||
this.hideFrame();
|
||||
this.showContainer(false);
|
||||
}
|
||||
}
|
||||
|
||||
startVideo() {
|
||||
if (this.video) this.video.srcObject = this.stream;
|
||||
}
|
||||
|
||||
parseResults(results: DetectedBarcode[]) {
|
||||
const code = results.length > 0 ? results[0] : undefined;
|
||||
if (code && code.rawValue !== '') {
|
||||
clearTimeout(this.scanId);
|
||||
this.code = code;
|
||||
this.capturing = false;
|
||||
|
||||
setTimeout(() => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('result', {
|
||||
detail: { code },
|
||||
})
|
||||
);
|
||||
this.stopScan();
|
||||
}, 300);
|
||||
} else {
|
||||
this.scanId = setTimeout(this.getResults.bind(this), 300);
|
||||
}
|
||||
}
|
||||
|
||||
async getResults() {
|
||||
if (this.code) {
|
||||
clearTimeout(this.scanId);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.video && this.detector) {
|
||||
let imgData: ImageData | null = this.getImageData(this.video);
|
||||
if (imgData) {
|
||||
const results = await this.detector.detect(imgData);
|
||||
imgData = null;
|
||||
this.parseResults(results);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('error', e);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getImageData(source: HTMLVideoElement) {
|
||||
if (this.dataCtx) {
|
||||
this.dataCtx.drawImage(source, 0, 0, this.dataCanvas.width, this.dataCanvas.height);
|
||||
return this.dataCtx.getImageData(0, 0, this.dataCanvas.width, this.dataCanvas.height);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
showContainer(show: boolean) {
|
||||
this.container.style.display = show ? 'block' : 'none';
|
||||
}
|
||||
|
||||
showFrame() {
|
||||
this.frame.style.display = 'block';
|
||||
}
|
||||
|
||||
hideFrame() {
|
||||
this.frame.style.display = 'none';
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,13 @@
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<scan-loading-screen></scan-loading-screen>
|
||||
<app-root></app-root>
|
||||
<script src="/scan-loading-screen/scan-loading-screen.js" fetchpriority="high"></script>
|
||||
<script>
|
||||
document.addEventListener('ng-boot', () => {
|
||||
document.querySelector('scan-loading-screen').remove();
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { App } from './app/app';
|
||||
import { BarcodeReader } from './app/web-components/barcode-reader';
|
||||
|
||||
bootstrapApplication(App, appConfig)
|
||||
.then(() => customElements.define('barcode-reader', BarcodeReader))
|
||||
.catch((err) => console.error(err));
|
||||
|
||||
56
src/migrations/20260117.ts
Normal file
56
src/migrations/20260117.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
const chainSql =
|
||||
'CREATE TABLE IF NOT EXISTS chain ( ' +
|
||||
'id INTEGER PRIMARY KEY AUTOINCREMENT, ' +
|
||||
'image TEXT, ' +
|
||||
'name TEXT ' +
|
||||
');';
|
||||
|
||||
const establishmentSql =
|
||||
'CREATE TABLE IF NOT EXISTS establishment ( ' +
|
||||
'id INTEGER PRIMARY KEY AUTOINCREMENT ,' +
|
||||
'address TEXT NOT NULL, ' +
|
||||
'chain_id INTEGER NOT NULL, ' +
|
||||
'FOREIGN KEY (chain_id) REFERENCES chain(id) ON DELETE CASCADE ' +
|
||||
');';
|
||||
|
||||
const establishmentChainIdxSql =
|
||||
'CREATE INDEX IF NOT EXISTS idx_establishment_chain_id ON establishment(chain_id);';
|
||||
|
||||
const productSql =
|
||||
'CREATE TABLE IF NOT EXISTS product ( ' +
|
||||
'id INTEGER PRIMARY KEY AUTOINCREMENT, ' +
|
||||
'barcode TEXT NOT NULL UNIQUE, ' +
|
||||
'name TEXT NOT NULL DEFAULT "", ' +
|
||||
'image TEXT ' +
|
||||
');';
|
||||
|
||||
const productEstablishmentSql =
|
||||
'CREATE TABLE IF NOT EXISTS product_establishment ( ' +
|
||||
'id INTEGER PRIMARY KEY AUTOINCREMENT, ' +
|
||||
'product_id INTEGER NOT NULL, ' +
|
||||
'establishment_id INTEGER NOT NULL, ' +
|
||||
'FOREIGN KEY (product_id) REFERENCES product(id) ON DELETE CASCADE, ' +
|
||||
'FOREIGN KEY (establishment_id) REFERENCES establishment(id) ON DELETE CASCADE, ' +
|
||||
'UNIQUE(product_id, establishment_id) ' +
|
||||
');';
|
||||
|
||||
const purchaseSql =
|
||||
'CREATE TABLE IF NOT EXISTS purchase ( ' +
|
||||
'id INTEGER PRIMARY KEY AUTOINCREMENT, ' +
|
||||
'establishment_id INTEGER NOT NULL, ' +
|
||||
'product_id INTEGER NOT NULL, ' +
|
||||
'price INTEGER NOT NULL DEFAULT 0, ' +
|
||||
'quantity INTEGER NOT NULL DEFAULT 1, ' +
|
||||
'date INTEGER NOT NULL, ' +
|
||||
'FOREIGN KEY (establishment_id) REFERENCES establishment(id) ON DELETE CASCADE, ' +
|
||||
'FOREIGN KEY (product_id) REFERENCES product(id) ON DELETE CASCADE ' +
|
||||
');';
|
||||
|
||||
export const tables: [string, any[]][] = [
|
||||
[chainSql, []],
|
||||
[establishmentSql, []],
|
||||
[establishmentChainIdxSql, []],
|
||||
[productSql, []],
|
||||
[productEstablishmentSql, []],
|
||||
[purchaseSql, []],
|
||||
];
|
||||
@@ -1 +1,4 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
html, body {
|
||||
margin: 0 0;
|
||||
height: 100%;
|
||||
}
|
||||
Reference in New Issue
Block a user