1
0
mirror of https://github.com/muety/wakapi.git synced 2023-08-10 21:12:56 +03:00

feat: basic integration / api tests (wip) (resolve #9)

This commit is contained in:
Ferdinand Mütsch
2021-05-28 17:13:28 +02:00
parent ee31212cdd
commit 1a808f9197
7 changed files with 1165 additions and 3 deletions

View File

@ -0,0 +1,879 @@
{
"info": {
"_postman_id": "472dcea5-a8b1-4507-8480-61644295c35b",
"name": "Wakapi API Tests",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "Auth",
"item": [
{
"name": "Sign up user",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code is 200\", function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Body matches string\", function () {",
" pm.expect(pm.response.text()).to.include(\"Account created successfully\");",
"});"
],
"type": "text/javascript"
}
}
],
"protocolProfileBehavior": {
"disableCookies": true
},
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "urlencoded",
"urlencoded": [
{
"key": "location",
"value": "{{TZ}}",
"type": "text"
},
{
"key": "username",
"value": "testuser",
"type": "text"
},
{
"key": "email",
"value": "testuser@wakapi.dev",
"type": "text"
},
{
"key": "password",
"value": "testpassword",
"type": "text"
},
{
"key": "password_repeat",
"value": "testpassword",
"type": "text"
}
]
},
"url": {
"raw": "{{BASE_URL}}/signup",
"host": [
"{{BASE_URL}}"
],
"path": [
"signup"
]
}
},
"response": []
},
{
"name": "Sign up existing user (conflict)",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code is 409\", function () {",
" pm.response.to.have.status(409);",
"});",
"",
"pm.test(\"Body matches string\", function () {",
" pm.expect(pm.response.text()).to.include(\"User already existing\");",
"});"
],
"type": "text/javascript"
}
}
],
"protocolProfileBehavior": {
"disableCookies": true
},
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "urlencoded",
"urlencoded": [
{
"key": "location",
"value": "{{TZ}}",
"type": "text"
},
{
"key": "username",
"value": "testuser",
"type": "text"
},
{
"key": "email",
"value": "testuser@wakapi.dev",
"type": "text"
},
{
"key": "password",
"value": "testpassword",
"type": "text"
},
{
"key": "password_repeat",
"value": "testpassword",
"type": "text"
}
]
},
"url": {
"raw": "{{BASE_URL}}/signup",
"host": [
"{{BASE_URL}}"
],
"path": [
"signup"
]
}
},
"response": []
},
{
"name": "Login",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code is 302\", function () {",
" pm.response.to.have.status(302);",
"});",
"",
"pm.test(\"Redirect to summary\", function () {",
" pm.expect(pm.response.headers.get(\"Location\")).to.eql(\"/summary\");",
"});",
"",
"pm.test(\"Sets cookie\", function () {",
" pm.expect(pm.response.headers.get(\"Set-Cookie\")).to.include(\"wakapi_auth=\");",
"});"
],
"type": "text/javascript"
}
}
],
"protocolProfileBehavior": {
"disableCookies": true,
"followRedirects": false
},
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "urlencoded",
"urlencoded": [
{
"key": "username",
"value": "testuser",
"type": "text"
},
{
"key": "password",
"value": "testpassword",
"type": "text"
}
]
},
"url": {
"raw": "{{BASE_URL}}/login",
"host": [
"{{BASE_URL}}"
],
"path": [
"login"
]
}
},
"response": []
},
{
"name": "Login (wrong password)",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code is 401\", function () {",
" pm.response.to.have.status(401);",
"});",
"",
"pm.test(\"No redirect\", function () {",
" pm.response.to.not.have.header(\"Location\");",
"});"
],
"type": "text/javascript"
}
}
],
"protocolProfileBehavior": {
"disableCookies": true,
"followRedirects": false
},
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "urlencoded",
"urlencoded": [
{
"key": "username",
"value": "testuser",
"type": "text"
},
{
"key": "password",
"value": "wrongpassword",
"type": "text"
}
]
},
"url": {
"raw": "{{BASE_URL}}/login",
"host": [
"{{BASE_URL}}"
],
"path": [
"login"
]
}
},
"response": []
}
]
},
{
"name": "Heartbeats",
"item": [
{
"name": "Create heartbeats",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code is 201\", function () {",
" pm.response.to.have.status(201);",
"});",
"",
"pm.test(\"Response body is correct\", function () {",
" var jsonData = pm.response.json();",
" pm.expect(jsonData.responses.length).to.eql(2);",
" pm.expect(jsonData.responses[0].length).to.eql(2);",
" pm.expect(jsonData.responses[1].length).to.eql(2);",
" pm.expect(jsonData.responses[0][1]).to.eql(201);",
" pm.expect(jsonData.responses[1][1]).to.eql(201);",
"});"
],
"type": "text/javascript"
}
}
],
"protocolProfileBehavior": {
"disableCookies": true
},
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{WRITEUSER_TOKEN}}",
"type": "string"
}
]
},
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "[{\n \"entity\": \"/home/user1/dev/proejct1/main.go\",\n \"project\": \"Project 1\",\n \"language\": \"Go\",\n \"is_write\": true,\n \"type\": \"file\",\n \"category\": null,\n \"branch\": null,\n \"time\": {{tsNowMinus1Min}}\n},\n{\n \"entity\": \"/home/user1/dev/proejct1/main.go\",\n \"project\": \"Project 1\",\n \"language\": \"Go\",\n \"is_write\": true,\n \"type\": \"file\",\n \"category\": null,\n \"branch\": null,\n \"time\": {{tsNowMinus2Min}}\n}]",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{BASE_URL}}/api/heartbeat",
"host": [
"{{BASE_URL}}"
],
"path": [
"api",
"heartbeat"
]
}
},
"response": []
},
{
"name": "Create heartbeats (unauthorized)",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code is 401\", function () {",
" pm.response.to.have.status(401);",
"});"
],
"type": "text/javascript"
}
}
],
"protocolProfileBehavior": {
"disableCookies": true
},
"request": {
"auth": {
"type": "noauth"
},
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "[{\n \"entity\": \"/home/user1/dev/proejct1/main.go\",\n \"project\": \"Project 1\",\n \"language\": \"Go\",\n \"is_write\": true,\n \"type\": \"file\",\n \"category\": null,\n \"branch\": null,\n \"time\": {{tsNowMinus1Min}}\n}]",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{BASE_URL}}/api/heartbeat",
"host": [
"{{BASE_URL}}"
],
"path": [
"api",
"heartbeat"
]
}
},
"response": []
}
]
},
{
"name": "Summary",
"item": [
{
"name": "Get summary (today)",
"event": [
{
"listen": "test",
"script": {
"exec": [
"const moment = require('moment')",
"",
"pm.test(\"Status code is 200\", function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Correct user\", function () {",
" const jsonData = pm.response.json();",
" pm.expect(jsonData.user_id).to.eql(\"writeuser\");",
"});",
"",
"pm.test(\"Correct summary data\", function () {",
" const jsonData = pm.response.json();",
" pm.expect(jsonData.projects.length).to.eql(1);",
" pm.expect(jsonData.languages.length).to.eql(1);",
" pm.expect(jsonData.editors.length).to.eql(1);",
" pm.expect(jsonData.operating_systems.length).to.eql(1);",
" pm.expect(jsonData.machines.length).to.eql(1);",
"});",
"",
"/*",
"// This is something the unit tests are supposed to check",
"pm.test(\"Correct summary range\", function () {",
" const jsonData = pm.response.json();",
" const from = moment(jsonData.from)",
" const to = moment(jsonData.to)",
"",
" pm.expect(moment.duration(moment().diff(from.add(2, 'm'))).asSeconds()).to.lt(10); // first heartbeat is now minus 1 min minus some latency",
" pm.expect(moment.duration(moment().diff(to.add(1, 'm'))).asSeconds()).to.lt(10); // first heartbeat is now minus 1 min minus some latency",
"});",
"*/"
],
"type": "text/javascript"
}
}
],
"protocolProfileBehavior": {
"tlsPreferServerCiphers": true,
"disableCookies": true
},
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{WRITEUSER_TOKEN}}",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "{{BASE_URL}}/api/summary?interval=today",
"host": [
"{{BASE_URL}}"
],
"path": [
"api",
"summary"
],
"query": [
{
"key": "interval",
"value": "today"
}
]
}
},
"response": []
},
{
"name": "Get summary (last 7 days)",
"event": [
{
"listen": "test",
"script": {
"exec": [
"const moment = require('moment')",
"",
"pm.test(\"Status code is 200\", function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Correct user\", function () {",
" const jsonData = pm.response.json();",
" pm.expect(jsonData.user_id).to.eql(\"writeuser\");",
"});",
"",
"pm.test(\"Correct summary data\", function () {",
" const jsonData = pm.response.json();",
" pm.expect(jsonData.projects.length).to.eql(1);",
" pm.expect(jsonData.languages.length).to.eql(1);",
" pm.expect(jsonData.editors.length).to.eql(1);",
" pm.expect(jsonData.operating_systems.length).to.eql(1);",
" pm.expect(jsonData.machines.length).to.eql(1);",
"});",
""
],
"type": "text/javascript"
}
}
],
"protocolProfileBehavior": {
"tlsPreferServerCiphers": true,
"disableCookies": true
},
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{WRITEUSER_TOKEN}}",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "{{BASE_URL}}/api/summary?interval=last_7_days",
"host": [
"{{BASE_URL}}"
],
"path": [
"api",
"summary"
],
"query": [
{
"key": "interval",
"value": "last_7_days"
}
]
}
},
"response": []
},
{
"name": "Get summary (week)",
"event": [
{
"listen": "test",
"script": {
"exec": [
"const moment = require('moment')",
"",
"pm.test(\"Status code is 200\", function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Correct user\", function () {",
" const jsonData = pm.response.json();",
" pm.expect(jsonData.user_id).to.eql(\"writeuser\");",
"});",
"",
"pm.test(\"Correct summary data\", function () {",
" const jsonData = pm.response.json();",
" pm.expect(jsonData.projects.length).to.eql(1);",
" pm.expect(jsonData.languages.length).to.eql(1);",
" pm.expect(jsonData.editors.length).to.eql(1);",
" pm.expect(jsonData.operating_systems.length).to.eql(1);",
" pm.expect(jsonData.machines.length).to.eql(1);",
"});",
""
],
"type": "text/javascript"
}
}
],
"protocolProfileBehavior": {
"tlsPreferServerCiphers": true,
"disableCookies": true
},
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{WRITEUSER_TOKEN}}",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "{{BASE_URL}}/api/summary?start=week",
"host": [
"{{BASE_URL}}"
],
"path": [
"api",
"summary"
],
"query": [
{
"key": "start",
"value": "week"
}
]
}
},
"response": []
},
{
"name": "Get summary (range)",
"event": [
{
"listen": "test",
"script": {
"exec": [
"const moment = require('moment')",
"",
"pm.test(\"Status code is 200\", function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Correct user\", function () {",
" const jsonData = pm.response.json();",
" pm.expect(jsonData.user_id).to.eql(\"writeuser\");",
"});",
"",
"pm.test(\"Correct summary data\", function () {",
" const jsonData = pm.response.json();",
" pm.expect(jsonData.projects.length).to.eql(1);",
" pm.expect(jsonData.languages.length).to.eql(1);",
" pm.expect(jsonData.editors.length).to.eql(1);",
" pm.expect(jsonData.operating_systems.length).to.eql(1);",
" pm.expect(jsonData.machines.length).to.eql(1);",
"});",
"",
"pm.test(\"Correct dates\", function () {",
" const jsonData = pm.response.json();",
" pm.expect(moment(jsonData.from).unix()).to.gt(moment(pm.variables.get('tsStartOfDayDate')).unix())",
" pm.expect(moment(jsonData.to).unix()).to.gt(moment(pm.variables.get('tsEndOfDayDate')).unix())",
"});",
""
],
"type": "text/javascript"
}
}
],
"protocolProfileBehavior": {
"tlsPreferServerCiphers": true,
"disableCookies": true
},
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{WRITEUSER_TOKEN}}",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "{{BASE_URL}}/api/summary?from={{tsStartOfDayDate}}&to={{tsEndOfTomorrowDate}}",
"host": [
"{{BASE_URL}}"
],
"path": [
"api",
"summary"
],
"query": [
{
"key": "from",
"value": "{{tsStartOfDayDate}}"
},
{
"key": "to",
"value": "{{tsEndOfTomorrowDate}}"
}
]
}
},
"response": []
},
{
"name": "Get summary (default tz)",
"event": [
{
"listen": "test",
"script": {
"exec": [
"const moment = require('moment')",
"",
"pm.test(\"Status code is 200\", function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Correct time zone\", function () {",
" const jsonData = pm.response.json();",
" const targetDateTz = moment(`2021-05-28T00:00:00${pm.variables.get('TZ_OFFSET')}`)",
" pm.expect(moment(jsonData.from).isSame(targetDateTz)).to.eql(true)",
"});",
""
],
"type": "text/javascript"
}
}
],
"protocolProfileBehavior": {
"tlsPreferServerCiphers": true,
"disableCookies": true,
"disableUrlEncoding": false
},
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{WRITEUSER_TOKEN}}",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "{{BASE_URL}}/api/summary?from=2021-05-28&to=2021-05-28",
"host": [
"{{BASE_URL}}"
],
"path": [
"api",
"summary"
],
"query": [
{
"key": "from",
"value": "2021-05-28"
},
{
"key": "to",
"value": "2021-05-28"
}
]
}
},
"response": []
},
{
"name": "Get summary (parse tz)",
"event": [
{
"listen": "test",
"script": {
"exec": [
"const moment = require('moment')",
"",
"pm.test(\"Status code is 200\", function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Correct time zone\", function () {",
" const jsonData = pm.response.json();",
" // when it was midnight in UTC+3, it was still 11 pm in Germany",
" const targetDateTz = moment(`2021-05-28T00:00:00${pm.variables.get('TZ_OFFSET')}`).add(-1, 'h')",
" pm.expect(moment(jsonData.from).isSame(targetDateTz)).to.eql(true)",
"});",
""
],
"type": "text/javascript"
}
}
],
"protocolProfileBehavior": {
"tlsPreferServerCiphers": true,
"disableCookies": true
},
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{WRITEUSER_TOKEN}}",
"type": "string"
}
]
},
"method": "GET",
"header": [],
"url": {
"raw": "{{BASE_URL}}/api/summary?from=2021-05-28T00:00:00%2B03:00&to=2021-05-28T00:00:00%2B03:00",
"host": [
"{{BASE_URL}}"
],
"path": [
"api",
"summary"
],
"query": [
{
"key": "from",
"value": "2021-05-28T00:00:00%2B03:00"
},
{
"key": "to",
"value": "2021-05-28T00:00:00%2B03:00"
}
]
}
},
"response": []
}
]
}
],
"event": [
{
"listen": "prerequest",
"script": {
"type": "text/javascript",
"exec": [
"const moment = require('moment')",
"",
"const now = moment()",
"const startOfDay = moment().startOf('day')",
"const endOfDay = moment().endOf('day')",
"const endOfTomorrow = moment().add(1, 'd').endOf('day')",
"",
"console.log(`Current timestamp is: ${now.format('x') / 1000}`)",
"",
"",
"// Auth stuff",
"const readApiKey = pm.variables.get('READUSER_API_KEY')",
"const writeApiKey = pm.variables.get('WRITEUSER_API_KEY')",
"",
"if (!readApiKey || !writeApiKey) {",
" throw new Error('no api key given')",
"}",
"",
"pm.variables.set('READUSER_TOKEN', base64encode(readApiKey))",
"pm.variables.set('WRITEUSER_TOKEN', base64encode(writeApiKey))",
"",
"function base64encode(str) {",
" return Buffer.from(str, 'utf-8').toString('base64')",
"}",
"",
"// Heartbeat stuff",
"pm.variables.set('tsNow', now.format('x') / 1000)",
"pm.variables.set('tsNowMinus1Min', now.add(-1, 'm').format('x') / 1000)",
"pm.variables.set('tsNowMinus2Min', now.add(-2, 'm').format('x') / 1000)",
"pm.variables.set('tsNowMinus3Min', now.add(-3, 'm').format('x') / 1000)",
"pm.variables.set('tsStartOfDay', startOfDay.format('x') / 1000)",
"pm.variables.set('tsEndOfDay', endOfDay.format('x') / 1000)",
"pm.variables.set('tsEndOfTomorrow', endOfTomorrow.format('x') / 1000)",
"pm.variables.set('tsStartOfDayIso', startOfDay.toISOString())",
"pm.variables.set('tsEndOfDayIso', endOfDay.toISOString())",
"pm.variables.set('tsEndOfTomorrowIso', endOfTomorrow.toISOString())",
"pm.variables.set('tsStartOfDayDate', startOfDay.format('YYYY-MM-DD'))",
"pm.variables.set('tsEndOfDayDate', endOfDay.format('YYYY-MM-DD'))",
"pm.variables.set('tsEndOfTomorrowDate', endOfTomorrow.format('YYYY-MM-DD'))"
]
}
},
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
""
]
}
}
],
"variable": [
{
"key": "BASE_URL",
"value": "http://localhost:3000"
},
{
"key": "READUSER_API_KEY",
"value": "33e7f538-0dce-4eba-8ffe-53db6814ed42"
},
{
"key": "WRITEUSER_API_KEY",
"value": "f7aa255c-8647-4d0b-b90f-621c58fd580f"
},
{
"key": "TZ",
"value": "Europe/Berlin"
},
{
"key": "TZ_OFFSET",
"value": "+02:00"
}
]
}

View File

@ -0,0 +1,61 @@
env: production
server:
listen_ipv4: 127.0.0.1
listen_ipv6:
tls_cert_path:
tls_key_path:
port: 3000
base_path: /
public_url: http://localhost:3000
app:
aggregation_time: '02:15'
report_time_weekly: 'fri,18:00'
inactive_days: 7
custom_languages:
vue: Vue
jsx: JSX
svelte: Svelte
db:
host:
port:
user:
password:
name: wakapi_testing.db
dialect: sqlite3
charset:
max_conn: 2
ssl: false
automgirate_fail_silently: false
security:
password_salt:
insecure_cookies: true
cookie_max_age: 172800
allow_signup: true
expose_metrics: false
sentry:
dsn:
enable_tracing: false
sample_rate:
sample_rate_heartbeats:
mail:
enabled: false
provider: smtp
sender: Wakapi <noreply@wakapi.dev>
smtp:
host:
port:
username:
password:
tls:
mailwhale:
url:
client_id:
client_secret:

4
testing/data.sql Normal file
View File

@ -0,0 +1,4 @@
BEGIN TRANSACTION;
INSERT INTO "users" ("id","api_key","email","location","password","created_at","last_logged_in_at","share_data_max_days","share_editors","share_languages","share_projects","share_oss","share_machines","is_admin","has_data","wakatime_api_key","reset_token","reports_weekly") VALUES ('readuser','33e7f538-0dce-4eba-8ffe-53db6814ed42','','Europe/Berlin','$2a$10$RCyfAFdlZdFJVWbxKz4f2uJ/MospiE1EFAIjvRizC4Nop9GfjgKzW','2021-05-28 12:34:25','2021-05-28 14:34:34.178+02:00',0,0,0,0,0,0,0,0,'','',0);
INSERT INTO "users" ("id","api_key","email","location","password","created_at","last_logged_in_at","share_data_max_days","share_editors","share_languages","share_projects","share_oss","share_machines","is_admin","has_data","wakatime_api_key","reset_token","reports_weekly") VALUES ('writeuser','f7aa255c-8647-4d0b-b90f-621c58fd580f','','Europe/Berlin','$2a$10$vsksPpiXZE9/xG9pRrZP.eKkbe/bGWW4wpPoXqvjiImZqMbN5c4Km','2021-05-28 12:34:56','2021-05-28 14:35:05.118+02:00',0,0,0,0,0,0,0,1,'','',0);
COMMIT;

40
testing/run_api_tests.sh Executable file
View File

@ -0,0 +1,40 @@
#!/bin/bash
if [ ! -f "wakapi" ]; then
echo "Wakapi executable not found. Run 'go build' first."
exit 1
fi
if ! command -v newman &> /dev/null
then
echo "Newman could not be found. Run 'npm install -g newman' first."
exit 1
fi
cd "$(dirname "$0")"
echo "Creating database and schema ..."
sqlite3 wakapi_testing.db < schema.sql
echo "Importing seed data ..."
sqlite3 wakapi_testing.db < data.sql
echo "Running Wakapi testing instance in background ..."
screen -S wakapi_testing -dm bash -c "../wakapi -config config.testing.yml"
echo "Waiting for Wakapi to come up ..."
until $(curl --output /dev/null --silent --get --fail http://localhost:3000/api/health); do
printf '.'
sleep 1
done
echo ""
echo "Running test collection ..."
newman run "Wakapi API Tests.postman_collection.json"
echo "Shutting down Wakapi ..."
screen -S wakapi_testing -X quit
echo "Deleting database ..."
rm wakapi_testing.db

147
testing/schema.sql Normal file
View File

@ -0,0 +1,147 @@
BEGIN TRANSACTION;
DROP TABLE IF EXISTS "users";
CREATE TABLE IF NOT EXISTS "users" (
"id" text,
"api_key" text UNIQUE,
"email" text,
"password" text,
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP,
"last_logged_in_at" timestamp DEFAULT CURRENT_TIMESTAMP,
"share_data_max_days" integer DEFAULT 0,
"share_editors" numeric DEFAULT false,
"share_languages" numeric DEFAULT false,
"share_projects" numeric DEFAULT false,
"share_oss" numeric DEFAULT false,
"share_machines" numeric DEFAULT false,
"is_admin" numeric DEFAULT false,
"has_data" numeric DEFAULT false,
"wakatime_api_key" text,
"reset_token" text,
"location" text,
"reports_weekly" numeric DEFAULT false,
PRIMARY KEY("id")
);
DROP TABLE IF EXISTS "key_string_values";
CREATE TABLE IF NOT EXISTS "key_string_values" (
"key" text,
"value" text,
PRIMARY KEY("key")
);
DROP TABLE IF EXISTS "summary_items";
CREATE TABLE IF NOT EXISTS "summary_items" (
"id" integer,
"summary_id" integer,
"type" integer,
"key" text,
"total" integer,
CONSTRAINT "fk_summaries_languages" FOREIGN KEY("summary_id") REFERENCES "summaries"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "fk_summary_items_summary" FOREIGN KEY("summary_id") REFERENCES "summaries"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "fk_summaries_machines" FOREIGN KEY("summary_id") REFERENCES "summaries"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "fk_summaries_projects" FOREIGN KEY("summary_id") REFERENCES "summaries"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "fk_summaries_operating_systems" FOREIGN KEY("summary_id") REFERENCES "summaries"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "fk_summaries_editors" FOREIGN KEY("summary_id") REFERENCES "summaries"("id") ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY("id")
);
DROP TABLE IF EXISTS "aliases";
CREATE TABLE IF NOT EXISTS "aliases" (
"id" integer,
"type" integer NOT NULL,
"user_id" text NOT NULL,
"key" text NOT NULL,
"value" text NOT NULL,
CONSTRAINT "fk_aliases_user" FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY("id")
);
DROP TABLE IF EXISTS "heartbeats";
CREATE TABLE IF NOT EXISTS "heartbeats" (
"id" integer,
"user_id" text NOT NULL,
"entity" text NOT NULL,
"type" text,
"category" text,
"project" text,
"branch" text,
"language" text,
"is_write" numeric,
"editor" text,
"operating_system" text,
"machine" text,
"time" timestamp,
"hash" varchar(17),
"origin" text,
"origin_id" text,
"created_at" timestamp,
CONSTRAINT "fk_heartbeats_user" FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY("id")
);
DROP TABLE IF EXISTS "summaries";
CREATE TABLE IF NOT EXISTS "summaries" (
"id" integer,
"user_id" text NOT NULL,
"from_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"to_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "fk_summaries_user" FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY("id")
);
DROP TABLE IF EXISTS "language_mappings";
CREATE TABLE IF NOT EXISTS "language_mappings" (
"id" integer,
"user_id" text NOT NULL,
"extension" varchar(16),
"language" varchar(64),
CONSTRAINT "fk_language_mappings_user" FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY("id")
);
DROP INDEX IF EXISTS "idx_user_email";
CREATE INDEX IF NOT EXISTS "idx_user_email" ON "users" (
"email"
);
DROP INDEX IF EXISTS "idx_type";
CREATE INDEX IF NOT EXISTS "idx_type" ON "summary_items" (
"type"
);
DROP INDEX IF EXISTS "idx_alias_type_key";
CREATE INDEX IF NOT EXISTS "idx_alias_type_key" ON "aliases" (
"type",
"key"
);
DROP INDEX IF EXISTS "idx_alias_user";
CREATE INDEX IF NOT EXISTS "idx_alias_user" ON "aliases" (
"user_id"
);
DROP INDEX IF EXISTS "idx_time";
CREATE INDEX IF NOT EXISTS "idx_time" ON "heartbeats" (
"time"
);
DROP INDEX IF EXISTS "idx_heartbeats_hash";
CREATE UNIQUE INDEX IF NOT EXISTS "idx_heartbeats_hash" ON "heartbeats" (
"hash"
);
DROP INDEX IF EXISTS "idx_time_user";
CREATE INDEX IF NOT EXISTS "idx_time_user" ON "heartbeats" (
"user_id"
);
DROP INDEX IF EXISTS "idx_entity";
CREATE INDEX IF NOT EXISTS "idx_entity" ON "heartbeats" (
"entity"
);
DROP INDEX IF EXISTS "idx_language";
CREATE INDEX IF NOT EXISTS "idx_language" ON "heartbeats" (
"language"
);
DROP INDEX IF EXISTS "idx_time_summary_user";
CREATE INDEX IF NOT EXISTS "idx_time_summary_user" ON "summaries" (
"user_id",
"from_time",
"to_time"
);
DROP INDEX IF EXISTS "idx_language_mapping_composite";
CREATE UNIQUE INDEX IF NOT EXISTS "idx_language_mapping_composite" ON "language_mappings" (
"user_id",
"extension"
);
DROP INDEX IF EXISTS "idx_language_mapping_user";
CREATE INDEX IF NOT EXISTS "idx_language_mapping_user" ON "language_mappings" (
"user_id"
);
COMMIT;