Database Schema#

Heedy uses an sqlite database, which is located at data/heedy.db in the heedy database folder. Any plugins that access or modify the database should have sqlite’s foreign keys on, and be compiled with the json1 extension.

Core Schema#

-- This is a meta-table, which specifies the versions of database schema in the database
-- Every plugin that includes tables in the core database must add itself to the table
CREATE TABLE dbversion (
	plugin VARCHAR(36) PRIMARY KEY NOT NULL,
	version INTEGER
);

-- This makes sure that the heedy version is specified, so that future upgrades will know
-- whether a schema modification is necessary
INSERT INTO dbversion VALUES ('heedy',1);

CREATE TABLE users (
	username VARCHAR(36) PRIMARY KEY NOT NULL,
	name VARCHAR NOT NULL DEFAULT '',
	description VARCHAR NOT NULL DEFAULT '',
	icon VARCHAR NOT NULL DEFAULT '',

	-- whether the public or users can read the user
	public_read BOOLEAN NOT NULL DEFAULT FALSE,
	users_read BOOLEAN NOT NULL DEFAULT FALSE,

	-- bcrypt-encoded password hash
	password VARCHAR NOT NULL,

	UNIQUE(username)
);

CREATE INDEX useraccess ON users(public_read,users_read);

CREATE TABLE apps (
	id VARCHAR(36) UNIQUE NOT NULL PRIMARY KEY,

	name VARCHAR NOT NULL,
	description VARCHAR NOT NULL DEFAULT '',
	icon VARCHAR NOT NULL DEFAULT '',

	owner VARACHAR(36) NOT NULL,

	-- Can (but does not have to) have an access token
	access_token VARCHAR UNIQUE DEFAULT NULL,

	created_date DATE NOT NULL DEFAULT CURRENT_DATE,
	-- apps without access tokens don't have access dates
	-- an app that has not yet logged in has this as null
	last_access_date DATE DEFAULT NULL, 

	-- Permissions are granted to a app through scope
	scope VARCHAR NOT NULL DEFAULT '[]',

	settings VARCHAR NOT NULL DEFAULT '{}',
	settings_schema VARCHAR NOT NULL DEFAULT '{}',

	enabled BOOLEAN NOT NULL DEFAULT TRUE,

	-- the "plugin key" of the app if it was generated for a plugin
	plugin VARCHAR DEFAULT NULL,

	CONSTRAINT valid_settings CHECK (json_valid(settings) AND json_type(settings)='object'),
	CONSTRAINT valid_settings_schema CHECK (json_valid(settings_schema)  AND json_type(settings)='object'),

	CONSTRAINT appowner
		FOREIGN KEY(owner) 
		REFERENCES users(username)
		ON UPDATE CASCADE
		ON DELETE CASCADE
);
-- We will want to list apps by owner 
CREATE INDEX appowner ON apps(owner,name);
-- A lot of querying will happen by API key
CREATE INDEX apptoken ON apps(access_token);


CREATE TABLE objects (
	id VARCHAR(36) UNIQUE NOT NULL PRIMARY KEY,
	name VARCHAR NOT NULL,
	description VARCHAR NOT NULL DEFAULT '',
	icon VARCHAR NOT NULL DEFAULT '',
	app VARCHAR(36) DEFAULT NULL,
	owner VARCHAR(36) NOT NULL,
	
	-- key to be used by the app/plugin to reference the object
	-- If not set, it is null, to allow unique constraint
	key VARCHAR(36) DEFAULT NULL,

	-- Tags are used for apps to easily query objects 
	tags VARCHAR NOT NULL DEFAULT '[]',

	type VARCHAR NOT NULL, 	                 -- The object type
	meta VARCHAR NOT NULL DEFAULT '{}',      -- Metadata for the object
	created_date DATE NOT NULL DEFAULT CURRENT_DATE,
	modified_date DATE DEFAULT NULL,		 -- Modification date for the object

	-- Maximal scope that the owner has
	owner_scope VARCHAR NOT NULL DEFAULT '["*"]',

	CONSTRAINT objectapp
		FOREIGN KEY(app) 
		REFERENCES apps(id)
		ON UPDATE CASCADE
		ON DELETE CASCADE,

	CONSTRAINT objectowner
		FOREIGN KEY(owner) 
		REFERENCES users(username)
		ON UPDATE CASCADE
		ON DELETE CASCADE,

	CONSTRAINT valid_tags CHECK (json_valid(tags)  AND json_type(tags)='array'),
	CONSTRAINT valid_scope CHECK (json_valid(owner_scope)  AND json_type(owner_scope)='array'),
	CONSTRAINT valid_meta CHECK (json_valid(meta) AND json_type(meta)='object'),

	-- The object key is unique per app. We use key first to have the index on key first
	UNIQUE(key,app),
	CONSTRAINT apps_only_key CHECK (key IS NULL OR app IS NOT NULL)
);

------------------------------------------------------------------------------------
-- SHARING
------------------------------------------------------------------------------------

CREATE TABLE shared_objects (
	username VARCHAR(36) NOT NULL,
	objectid VARCHAR(36) NOT NULL,
	scope VARCHAR NOT NULL DEFAULT '["read"]',

	PRIMARY KEY (username,objectid),
	UNIQUE (username,objectid),

	CONSTRAINT objectuser
		FOREIGN KEY(username)
		REFERENCES users(username)
		ON UPDATE CASCADE
		ON DELETE CASCADE,

	CONSTRAINT sharedobject
		FOREIGN KEY(objectid)
		REFERENCES objects(id)
		ON UPDATE CASCADE
		ON DELETE CASCADE,

	CONSTRAINT valid_scope CHECK (json_valid(scope) AND json_type(scope)='array')
);

CREATE INDEX share_objectid on shared_objects(objectid);


------------------------------------------------------------------
-- User Sessions
------------------------------------------------------------------
-- These are used to control manually logged in users,
-- so that we don't need to put passwords in cookies,
-- and can remotely log out from other browsers

CREATE TABLE user_sessions (
	username VARCHAR(36) NOT NULL,
	sessionid VARCHAR NOT NULL,

	token VARCHAR UNIQUE NOT NULL,

	description VARCHAR,
	created_date DATE NOT NULL DEFAULT CURRENT_DATE,
	last_access_date DATE NOT NULL DEFAULT CURRENT_DATE,

	CONSTRAINT pk PRIMARY KEY (username,sessionid),

	CONSTRAINT fk_user
		FOREIGN KEY(username) 
		REFERENCES users(username)
		ON UPDATE CASCADE
		ON DELETE CASCADE
);

-- This will be requested on every single query
CREATE INDEX user_login_tokens ON user_sessions(token);

------------------------------------------------------------------
-- User Settings
------------------------------------------------------------------
-- The settings are per-user k/v pairs for each plugin . Heedy settings
-- use the plugin 'heedy'. The schemas are defined in heedy.conf

CREATE TABLE user_settings (
	user VARCHAR NOT NULL,

	plugin VARCHAR NOT NULL,
	key VARCHAR NOT NULL,
	value VARCHAR NOT NULL DEFAULT 'null',

	CONSTRAINT pk PRIMARY KEY (user,plugin,key),
	CONSTRAINT valid_value CHECK(json_valid(value)),

	CONSTRAINT fk
		FOREIGN KEY (user)
		REFERENCES users(username)
		ON UPDATE CASCADE
		ON DELETE CASCADE
);


------------------------------------------------------------------
-- Database Views
------------------------------------------------------------------

CREATE VIEW user_object_scope(user,object,scope) AS
	SELECT objects.owner,objects.id,'*' FROM objects WHERE objects.app IS NULL
	UNION ALL
	SELECT objects.owner,objects.id,value FROM objects,json_each(objects.owner_scope) WHERE objects.app IS NOT NULL
	UNION ALL
	SELECT shared_objects.username,objects.id,ss.value FROM objects,shared_objects,json_each(shared_objects.scope) AS ss WHERE shared_objects.objectid=objects.id AND ss.value<>'*' AND EXISTS (SELECT sss.value FROM json_each(objects.owner_scope) AS sss WHERE sss.value=ss.value OR sss.value='*')
	UNION ALL
	SELECT shared_objects.username,objects.id,sss.value FROM objects,shared_objects,json_each(objects.owner_scope) AS sss WHERE shared_objects.objectid=objects.id AND EXISTS (SELECT 1 FROM json_each(shared_objects.scope) AS ss WHERE ss.value='*')
	;


------------------------------------------------------------------
-- Database Default Users
------------------------------------------------------------------

INSERT INTO users (username,name,description,icon,password) VALUES 
-- The public/users virtual users are created by default, and cannot be deleted,
-- as they represent the database view that someone not logged in will get,
-- and the objects accessible to a user who is logged in
(
	'users',
	'Users',
	'All logged-in users',
	'perm_identity',
	'-'
),(
	'public',
	'Public',
	'Represents everyone - any visitor to this heedy server',
	'share',
	'-'
),
-- The heedy user represents the database internals. It is used as the actor
-- when the software or plugins do something 
(
	"heedy",
	"Heedy",
	"",
	"remove_red_eye",
	"-"
);

Timeseries#

CREATE TABLE timeseries (
	tsid VARCHAR(36) NOT NULL,
	tstart REAL NOT NULL,
	tend REAL NOT NULL,
	length INTEGER NOT NULL,

	-- timeseries data comes as zstandard-compressed msgpack array batches
	data BLOB,

	PRIMARY KEY (tsid,tstart),
	CONSTRAINT valid_range CHECK (tstart <= tend AND length > 0),

	CONSTRAINT object_fk
		FOREIGN KEY(tsid)
		REFERENCES objects(id)
		ON UPDATE CASCADE
		ON DELETE CASCADE

);
CREATE INDEX timeseries_duration ON timeseries(tsid,tend,tstart);

Dashboards#

CREATE TABLE dashboard_elements (
	object_id VARCHAR(36) NOT NULL,
	element_id VARCHAR(36) NOT NULL,

	element_index INT NOT NULL,

	-- The element type specifies the API call to make for backend data
	type VARCHAR NOT NULL,

	-- To save on computation, dashboards are updated on-demand
	outdated BOOL NOT NULL DEFAULT TRUE,
	on_demand BOOL NOT NULL DEFAULT TRUE,

	title VARCHAR(100) NOT NULL,

	-- The query to run on the backend to update data
	query BLOB NOT NULL,
	-- Saved output of query, compressed with zstandard (can be large)
	data BLOB DEFAULT NULL,
	-- Settings for displaying the data on the frontend
	settings BLOB NOT NULL,

	PRIMARY KEY (object_id,element_id),

	CONSTRAINT all_valid CHECK (json_valid(query) AND json_valid(settings)),

	CONSTRAINT object_updater
		FOREIGN KEY(object_id)
		REFERENCES objects(id)
		ON UPDATE CASCADE
		ON DELETE CASCADE
);

CREATE TABLE dashboard_events (
	object_id VARCHAR(36) NOT NULL,
	element_id VARCHAR(36) NOT NULL,

	-- The event
	event VARCHAR NOT NULL,
	event_object_id VARCHAR NOT NULL,

	PRIMARY KEY (object_id,element_id,event_object_id,event),

	CONSTRAINT underlying_element
		FOREIGN KEY(object_id,element_id)
		REFERENCES dashboard_elements(object_id,element_id)
		ON UPDATE CASCADE
		ON DELETE CASCADE,
	CONSTRAINT event_object_c
		FOREIGN KEY(event_object_id)
		REFERENCES objects(id)
		ON UPDATE CASCADE
		ON DELETE CASCADE
);
CREATE INDEX events_idx ON dashboard_events(event_object_id,event);

Notifications#

-- We split up the schema into 3 tables due to issues with UNIQUE when certain values are NULL.
-- We need apps/objects to be nullable to represent notifications for users/apps
-- https://stackoverflow.com/questions/22699409/sqlite-null-and-unique

CREATE TABLE notifications_user (
	user VARCHAR NOT NULL,
	key VARCHAR NOT NULL,

	title VARCHAR NOT NULL,
	description VARCHAR NOT NULL DEFAULT '',
	type VARCHAR NOT NULL DEFAULT 'info',
	timestamp REAL NOT NULL,
	actions VARCHAR NOT NULL DEFAULT '[]',

	-- User notifications are global=true
	global BOOLEAN NOT NULL DEFAULT true,
	dismissible BOOLEAN NOT NULL DEFAULT true,
	seen BOOLEAN NOT NULL DEFAULT false,

	CONSTRAINT pk PRIMARY KEY (user,key),
	CONSTRAINT valid_actions CHECK(json_valid(actions) AND json_type(actions)=='array'),

	CONSTRAINT user_c
		FOREIGN KEY (user)
		REFERENCES users(username)
		ON UPDATE CASCADE
		ON DELETE CASCADE
);

CREATE TABLE notifications_app (
	user VARCHAR NOT NULL,
	app VARCHAR NOT NULL,
	key VARCHAR NOT NULL,

	title VARCHAR NOT NULL,
	description VARCHAR NOT NULL DEFAULT '',
	type VARCHAR NOT NULL DEFAULT 'info',
	timestamp REAL NOT NULL,
	actions VARCHAR NOT NULL DEFAULT '[]',

	global BOOLEAN NOT NULL DEFAULT false,
	seen BOOLEAN NOT NULL DEFAULT false,
	dismissible BOOLEAN NOT NULL DEFAULT true,

	CONSTRAINT pk PRIMARY KEY (user,app,key),
	CONSTRAINT valid_actions CHECK(json_valid(actions) AND json_type(actions)=='array'),

	CONSTRAINT user_c
		FOREIGN KEY (user)
		REFERENCES users(username)
		ON UPDATE CASCADE
		ON DELETE CASCADE,

	CONSTRAINT app_c
		FOREIGN KEY (app)
		REFERENCES apps(id)
		ON UPDATE CASCADE
		ON DELETE CASCADE
);

CREATE TABLE notifications_object (
	user VARCHAR NOT NULL,
	app VARCHAR NOT NULL,
	object VARCHAR NOT NULL,
	key VARCHAR NOT NULL,

	title VARCHAR NOT NULL,
	description VARCHAR NOT NULL DEFAULT '',
	type VARCHAR NOT NULL DEFAULT 'info',
	actions VARCHAR NOT NULL DEFAULT '[]',
	timestamp REAL NOT NULL,

	global BOOLEAN NOT NULL DEFAULT false,
	seen BOOLEAN NOT NULL DEFAULT false,
	dismissible BOOLEAN NOT NULL DEFAULT true,

	CONSTRAINT pk PRIMARY KEY (user,app,object,key),
	CONSTRAINT valid_actions CHECK(json_valid(actions) AND json_type(actions)=='array'),

	CONSTRAINT user_c
		FOREIGN KEY (user)
		REFERENCES users(username)
		ON UPDATE CASCADE
		ON DELETE CASCADE,

	CONSTRAINT app_c
		FOREIGN KEY (app)
		REFERENCES apps(id)
		ON UPDATE CASCADE
		ON DELETE CASCADE,

	CONSTRAINT object_c
		FOREIGN KEY (object)
		REFERENCES objects(id)
		ON UPDATE CASCADE
		ON DELETE CASCADE
);

Key-Value Storage#

CREATE TABLE kv_user (
	user VARCHAR NOT NULL,

	namespace VARCHAR NOT NULL,
	key VARCHAR NOT NULL,
	value VARCHAR NOT NULL DEFAULT 'null',

	CONSTRAINT pk PRIMARY KEY (user,namespace,key),
	CONSTRAINT valid_value CHECK(json_valid(value)),

	CONSTRAINT fk
		FOREIGN KEY (user)
		REFERENCES users(username)
		ON UPDATE CASCADE
		ON DELETE CASCADE
);

CREATE TABLE kv_app (
	app VARCHAR NOT NULL,

	namespace VARCHAR NOT NULL,
	key VARCHAR NOT NULL,
	value VARCHAR NOT NULL DEFAULT 'null',

	CONSTRAINT pk PRIMARY KEY (app,namespace,key),
	CONSTRAINT valid_value CHECK(json_valid(value)),

	CONSTRAINT fk
		FOREIGN KEY (app)
		REFERENCES apps(id)
		ON UPDATE CASCADE
		ON DELETE CASCADE
);

CREATE TABLE kv_object (
	object VARCHAR NOT NULL,

	namespace VARCHAR NOT NULL,
	key VARCHAR NOT NULL,
	value VARCHAR NOT NULL DEFAULT 'null',

	CONSTRAINT pk PRIMARY KEY (object,namespace,key),
	CONSTRAINT valid_value CHECK(json_valid(value)),

	CONSTRAINT fk
		FOREIGN KEY (object)
		REFERENCES objects(id)
		ON UPDATE CASCADE
		ON DELETE CASCADE
);