@tomorrowos/sdk 0.4.7 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,6 +38,15 @@ npm install
38
38
  npm run dev
39
39
  ```
40
40
 
41
+ `tomorrowos init` creates `data/tomorrowos.db` with the TomorrowOS SQLite
42
+ schema. The starter server uses that SQLite database by default, so pairings,
43
+ playlists, and device assignments survive normal server restarts.
44
+
45
+ For throwaway demos/tests, set `TOMORROWOS_STORE=memory`. For production cloud
46
+ databases such as Supabase/Postgres, implement `TomorrowOSStore` and pass it to
47
+ `new TomorrowOS({ brand, store })`; the SDK keeps persistence behind that
48
+ interface.
49
+
41
50
  Build a player (placeholder in current SDK — see `PLAYER_INSTALL.md` for platform tooling):
42
51
 
43
52
  ```bash
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import fs from "fs";
3
3
  import path from "path";
4
4
  import { fileURLToPath } from "url";
5
+ import { SQLiteStore } from "./store/sqlite-store.js";
5
6
  function packageRoot() {
6
7
  const here = path.dirname(fileURLToPath(import.meta.url));
7
8
  return path.resolve(here, "..");
@@ -32,6 +33,12 @@ function removeStarterArtifacts(destDir) {
32
33
  if (fs.existsSync(modulesPath))
33
34
  fs.rmSync(modulesPath, { recursive: true, force: true });
34
35
  }
36
+ function initializeStarterSQLite(destDir) {
37
+ const dbPath = path.join(path.resolve(destDir), "data", "tomorrowos.db");
38
+ const store = new SQLiteStore(dbPath);
39
+ store.close();
40
+ return dbPath;
41
+ }
35
42
  /** Align generated cms-starter package.json with the installed SDK version. */
36
43
  function patchStarterPackageJson(destDir) {
37
44
  const pkgPath = path.join(path.resolve(destDir), "package.json");
@@ -68,9 +75,11 @@ function copyStarter(destDir, force) {
68
75
  });
69
76
  patchStarterPackageJson(resolved);
70
77
  removeStarterArtifacts(resolved);
78
+ const dbPath = initializeStarterSQLite(resolved);
71
79
  const sdkVer = getSdkVersion();
72
80
  console.log(`[tomorrowos] Created CMS project at ${resolved}`);
73
81
  console.log(`[tomorrowos] @tomorrowos/sdk dependency: ^${sdkVer}`);
82
+ console.log(`[tomorrowos] Initialized SQLite database: ${dbPath}`);
74
83
  console.log("Next: cd there, run npm install, then npm start");
75
84
  }
76
85
  function cmdBuild(argv) {
package/dist/index.d.ts CHANGED
@@ -5,4 +5,7 @@ export { PlaylistCatalog } from "./playlist-catalog.js";
5
5
  export type { BuiltDevicePolicy, SavePlaylistInput } from "./playlist-catalog.js";
6
6
  export { generateRandomPairingCode, isValidPairingCodeFormat, normalizePairingCode, PAIRING_CODE_ALPHABET, PAIRING_CODE_LENGTH } from "./pairing-code.js";
7
7
  export { MemoryStore } from "./store/memory-store.js";
8
+ export { SQLiteStore } from "./store/sqlite-store.js";
9
+ export { createTomorrowOSStore, defaultSQLitePath } from "./store/factory.js";
10
+ export type { CreateTomorrowOSStoreOptions, TomorrowOSStoreDriver } from "./store/factory.js";
8
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EACV,cAAc,EACd,aAAa,EACb,eAAe,EACf,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EACV,wBAAwB,EACxB,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,yBAAyB,EACzB,cAAc,EACd,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EACL,yBAAyB,EACzB,wBAAwB,EACxB,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EACV,cAAc,EACd,aAAa,EACb,eAAe,EACf,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EACV,wBAAwB,EACxB,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,yBAAyB,EACzB,cAAc,EACd,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EACL,yBAAyB,EACzB,wBAAwB,EACxB,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EAClB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,4BAA4B,EAC5B,qBAAqB,EACtB,MAAM,oBAAoB,CAAC"}
package/dist/index.js CHANGED
@@ -2,3 +2,5 @@ export { TomorrowOS } from "./tomorrowos.js";
2
2
  export { PlaylistCatalog } from "./playlist-catalog.js";
3
3
  export { generateRandomPairingCode, isValidPairingCodeFormat, normalizePairingCode, PAIRING_CODE_ALPHABET, PAIRING_CODE_LENGTH } from "./pairing-code.js";
4
4
  export { MemoryStore } from "./store/memory-store.js";
5
+ export { SQLiteStore } from "./store/sqlite-store.js";
6
+ export { createTomorrowOSStore, defaultSQLitePath } from "./store/factory.js";
@@ -0,0 +1,20 @@
1
+ import type { TomorrowOSStore } from "./types.js";
2
+ export type TomorrowOSStoreDriver = "sqlite" | "memory";
3
+ export interface CreateTomorrowOSStoreOptions {
4
+ /**
5
+ * Defaults to TOMORROWOS_STORE, then sqlite.
6
+ * Use memory only for tests or throwaway demos.
7
+ */
8
+ driver?: TomorrowOSStoreDriver;
9
+ /** Defaults to TOMORROWOS_DB_PATH, then ./data/tomorrowos.db. */
10
+ sqlitePath?: string;
11
+ /**
12
+ * Reserved for external database adapters. Supabase exposes Postgres URLs,
13
+ * which should be handled by a Postgres TomorrowOSStore implementation.
14
+ */
15
+ databaseUrl?: string;
16
+ env?: NodeJS.ProcessEnv;
17
+ }
18
+ export declare function defaultSQLitePath(cwd?: string): string;
19
+ export declare function createTomorrowOSStore(options?: CreateTomorrowOSStoreOptions): TomorrowOSStore;
20
+ //# sourceMappingURL=factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/store/factory.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,MAAM,qBAAqB,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAExD,MAAM,WAAW,4BAA4B;IAC3C;;;OAGG;IACH,MAAM,CAAC,EAAE,qBAAqB,CAAC;IAC/B,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AAED,wBAAgB,iBAAiB,CAAC,GAAG,SAAgB,GAAG,MAAM,CAE7D;AAED,wBAAgB,qBAAqB,CACnC,OAAO,GAAE,4BAAiC,GACzC,eAAe,CAwBjB"}
@@ -0,0 +1,23 @@
1
+ import path from "path";
2
+ import { MemoryStore } from "./memory-store.js";
3
+ import { SQLiteStore } from "./sqlite-store.js";
4
+ export function defaultSQLitePath(cwd = process.cwd()) {
5
+ return path.join(cwd, "data", "tomorrowos.db");
6
+ }
7
+ export function createTomorrowOSStore(options = {}) {
8
+ const env = options.env ?? process.env;
9
+ const driver = (options.driver ?? env.TOMORROWOS_STORE ?? "sqlite").toLowerCase();
10
+ const databaseUrl = options.databaseUrl ?? env.DATABASE_URL;
11
+ if (driver === "memory") {
12
+ return new MemoryStore();
13
+ }
14
+ if (driver !== "sqlite") {
15
+ throw new Error(`Unsupported TOMORROWOS_STORE "${driver}". Use "sqlite", "memory", or pass a custom TomorrowOSStore to new TomorrowOS({ store }).`);
16
+ }
17
+ if (databaseUrl) {
18
+ throw new Error("DATABASE_URL is set, but @tomorrowos/sdk does not include a Postgres/Supabase adapter yet. Pass a custom TomorrowOSStore to new TomorrowOS({ store }) or unset DATABASE_URL to use SQLite.");
19
+ }
20
+ return new SQLiteStore({
21
+ databasePath: options.sqlitePath ?? env.TOMORROWOS_DB_PATH ?? defaultSQLitePath()
22
+ });
23
+ }
@@ -0,0 +1,39 @@
1
+ import type { DevicePlaylistAssignment, DeviceRegistryRecord, PairedDeviceEntry, PairedDeviceRecord, PendingCodeRecord, StoredPlaylist, TomorrowOSStore } from "./types.js";
2
+ interface SQLiteStoreOptions {
3
+ databasePath?: string;
4
+ }
5
+ /**
6
+ * Durable single-node store backed by a local SQLite database.
7
+ * For multi-instance production, inject a Postgres/Supabase-backed TomorrowOSStore instead.
8
+ */
9
+ export declare class SQLiteStore implements TomorrowOSStore {
10
+ readonly databasePath: string;
11
+ private readonly db;
12
+ constructor(options?: SQLiteStoreOptions | string);
13
+ init(): void;
14
+ close(): void;
15
+ setPendingCode(code: string, record: PendingCodeRecord): Promise<void>;
16
+ getPendingCode(code: string): Promise<PendingCodeRecord | undefined>;
17
+ deletePendingCode(code: string): Promise<void>;
18
+ getDeviceRegistry(deviceId: string): Promise<DeviceRegistryRecord | undefined>;
19
+ setDeviceRegistry(deviceId: string, record: DeviceRegistryRecord): Promise<void>;
20
+ getDeviceRegistryByCode(code: string): Promise<{
21
+ deviceId: string;
22
+ record: DeviceRegistryRecord;
23
+ } | undefined>;
24
+ setPairedDevice(deviceId: string, record: PairedDeviceRecord): Promise<void>;
25
+ getPairedDevice(deviceId: string): Promise<PairedDeviceRecord | undefined>;
26
+ deletePairedDevice(deviceId: string): Promise<void>;
27
+ listPairedDevices(): Promise<PairedDeviceEntry[]>;
28
+ listPlaylists(): Promise<StoredPlaylist[]>;
29
+ getPlaylist(id: string): Promise<StoredPlaylist | undefined>;
30
+ setPlaylist(record: StoredPlaylist): Promise<void>;
31
+ isPlaylistNameTaken(name: string, excludeId?: string): Promise<boolean>;
32
+ getDeviceAssignments(deviceId: string): Promise<DevicePlaylistAssignment[]>;
33
+ setDeviceAssignments(deviceId: string, assignments: DevicePlaylistAssignment[]): Promise<void>;
34
+ private mapDeviceRegistryRow;
35
+ private mapPairedDeviceRow;
36
+ private mapPlaylistRow;
37
+ }
38
+ export {};
39
+ //# sourceMappingURL=sqlite-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite-store.d.ts","sourceRoot":"","sources":["../../src/store/sqlite-store.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,wBAAwB,EACxB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EAGjB,cAAc,EACd,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB,UAAU,kBAAkB;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA8DD;;;GAGG;AACH,qBAAa,WAAY,YAAW,eAAe;IACjD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAoB;gBAE3B,OAAO,GAAE,kBAAkB,GAAG,MAAW;IAerD,IAAI,IAAI,IAAI;IAwEZ,KAAK,IAAI,IAAI;IAIP,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAUtE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAUpE,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C,iBAAiB,CACrB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC;IAStC,iBAAiB,CACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,IAAI,CAAC;IA2BV,uBAAuB,CAC3B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,oBAAoB,CAAA;KAAE,GAAG,SAAS,CAAC;IAapE,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,IAAI,CAAC;IAuCV,eAAe,CACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC;IASpC,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAInD,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAYjD,aAAa,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAS1C,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAS5D,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAiClD,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAcvE,oBAAoB,CACxB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,wBAAwB,EAAE,CAAC;IAoBhC,oBAAoB,CACxB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,wBAAwB,EAAE,GACtC,OAAO,CAAC,IAAI,CAAC;IA8BhB,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,kBAAkB;IAc1B,OAAO,CAAC,cAAc;CAYvB"}
@@ -0,0 +1,345 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import Database from "better-sqlite3";
4
+ const DEFAULT_SQLITE_PATH = path.join(process.cwd(), "data", "tomorrowos.db");
5
+ function optionalString(value) {
6
+ return value ?? undefined;
7
+ }
8
+ function optionalNumber(value) {
9
+ return value ?? undefined;
10
+ }
11
+ function parseJson(raw, fallback) {
12
+ if (!raw)
13
+ return fallback;
14
+ return JSON.parse(raw);
15
+ }
16
+ /**
17
+ * Durable single-node store backed by a local SQLite database.
18
+ * For multi-instance production, inject a Postgres/Supabase-backed TomorrowOSStore instead.
19
+ */
20
+ export class SQLiteStore {
21
+ databasePath;
22
+ db;
23
+ constructor(options = {}) {
24
+ const databasePath = typeof options === "string"
25
+ ? options
26
+ : options.databasePath ?? DEFAULT_SQLITE_PATH;
27
+ this.databasePath = databasePath === ":memory:" ? databasePath : path.resolve(databasePath);
28
+ if (this.databasePath !== ":memory:") {
29
+ fs.mkdirSync(path.dirname(this.databasePath), { recursive: true });
30
+ }
31
+ this.db = new Database(this.databasePath);
32
+ this.init();
33
+ }
34
+ init() {
35
+ this.db.pragma("foreign_keys = ON");
36
+ this.db.pragma("journal_mode = WAL");
37
+ this.db.pragma("busy_timeout = 5000");
38
+ this.db.exec(`
39
+ CREATE TABLE IF NOT EXISTS schema_migrations (
40
+ id INTEGER PRIMARY KEY,
41
+ name TEXT NOT NULL UNIQUE,
42
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
43
+ );
44
+
45
+ CREATE TABLE IF NOT EXISTS pending_codes (
46
+ code TEXT PRIMARY KEY,
47
+ device_id TEXT NOT NULL,
48
+ created_at INTEGER NOT NULL
49
+ );
50
+
51
+ CREATE TABLE IF NOT EXISTS device_registry (
52
+ device_id TEXT PRIMARY KEY,
53
+ permanent_pairing_code TEXT NOT NULL UNIQUE,
54
+ code_created_at INTEGER NOT NULL,
55
+ serial_number TEXT,
56
+ first_seen_at INTEGER,
57
+ last_hello_at INTEGER
58
+ );
59
+
60
+ CREATE TABLE IF NOT EXISTS paired_devices (
61
+ device_id TEXT PRIMARY KEY,
62
+ pairing_token TEXT NOT NULL,
63
+ paired_at TEXT NOT NULL,
64
+ device_name TEXT,
65
+ platform TEXT,
66
+ system TEXT,
67
+ last_boot_at TEXT,
68
+ last_online_at TEXT,
69
+ last_offline_at TEXT,
70
+ last_policy_push_at TEXT
71
+ );
72
+
73
+ CREATE TABLE IF NOT EXISTS playlists (
74
+ id TEXT PRIMARY KEY,
75
+ name TEXT NOT NULL,
76
+ schedule_json TEXT,
77
+ items_json TEXT NOT NULL DEFAULT '[]',
78
+ version INTEGER NOT NULL,
79
+ updated_at TEXT NOT NULL,
80
+ retired INTEGER NOT NULL DEFAULT 0,
81
+ retired_at TEXT
82
+ );
83
+
84
+ CREATE UNIQUE INDEX IF NOT EXISTS playlists_active_name_unique
85
+ ON playlists (lower(trim(name)))
86
+ WHERE retired = 0;
87
+
88
+ CREATE TABLE IF NOT EXISTS device_assignments (
89
+ device_id TEXT NOT NULL,
90
+ playlist_id TEXT NOT NULL,
91
+ sort_order INTEGER NOT NULL,
92
+ published_version INTEGER NOT NULL,
93
+ published_at TEXT NOT NULL,
94
+ snapshot_json TEXT NOT NULL,
95
+ PRIMARY KEY (device_id, playlist_id)
96
+ );
97
+
98
+ CREATE INDEX IF NOT EXISTS device_assignments_device_order_idx
99
+ ON device_assignments (device_id, sort_order);
100
+
101
+ INSERT OR IGNORE INTO schema_migrations (id, name)
102
+ VALUES (1, 'initial_tomorrowos_store');
103
+ `);
104
+ }
105
+ close() {
106
+ this.db.close();
107
+ }
108
+ async setPendingCode(code, record) {
109
+ this.db.prepare(`
110
+ INSERT INTO pending_codes (code, device_id, created_at)
111
+ VALUES (?, ?, ?)
112
+ ON CONFLICT(code) DO UPDATE SET
113
+ device_id = excluded.device_id,
114
+ created_at = excluded.created_at
115
+ `).run(code, record.deviceId, record.createdAt);
116
+ }
117
+ async getPendingCode(code) {
118
+ const row = this.db.prepare(`
119
+ SELECT device_id, created_at
120
+ FROM pending_codes
121
+ WHERE code = ?
122
+ `).get(code);
123
+ if (!row)
124
+ return undefined;
125
+ return { deviceId: row.device_id, createdAt: row.created_at };
126
+ }
127
+ async deletePendingCode(code) {
128
+ this.db.prepare("DELETE FROM pending_codes WHERE code = ?").run(code);
129
+ }
130
+ async getDeviceRegistry(deviceId) {
131
+ const row = this.db.prepare(`
132
+ SELECT *
133
+ FROM device_registry
134
+ WHERE device_id = ?
135
+ `).get(deviceId);
136
+ return row ? this.mapDeviceRegistryRow(row) : undefined;
137
+ }
138
+ async setDeviceRegistry(deviceId, record) {
139
+ this.db.prepare(`
140
+ INSERT INTO device_registry (
141
+ device_id,
142
+ permanent_pairing_code,
143
+ code_created_at,
144
+ serial_number,
145
+ first_seen_at,
146
+ last_hello_at
147
+ )
148
+ VALUES (?, ?, ?, ?, ?, ?)
149
+ ON CONFLICT(device_id) DO UPDATE SET
150
+ permanent_pairing_code = excluded.permanent_pairing_code,
151
+ code_created_at = excluded.code_created_at,
152
+ serial_number = excluded.serial_number,
153
+ first_seen_at = excluded.first_seen_at,
154
+ last_hello_at = excluded.last_hello_at
155
+ `).run(deviceId, record.permanentPairingCode, record.codeCreatedAt, record.serialNumber ?? null, record.firstSeenAt ?? null, record.lastHelloAt ?? null);
156
+ }
157
+ async getDeviceRegistryByCode(code) {
158
+ const row = this.db.prepare(`
159
+ SELECT *
160
+ FROM device_registry
161
+ WHERE permanent_pairing_code = ?
162
+ `).get(code);
163
+ if (!row)
164
+ return undefined;
165
+ return {
166
+ deviceId: row.device_id,
167
+ record: this.mapDeviceRegistryRow(row)
168
+ };
169
+ }
170
+ async setPairedDevice(deviceId, record) {
171
+ this.db.prepare(`
172
+ INSERT INTO paired_devices (
173
+ device_id,
174
+ pairing_token,
175
+ paired_at,
176
+ device_name,
177
+ platform,
178
+ system,
179
+ last_boot_at,
180
+ last_online_at,
181
+ last_offline_at,
182
+ last_policy_push_at
183
+ )
184
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
185
+ ON CONFLICT(device_id) DO UPDATE SET
186
+ pairing_token = excluded.pairing_token,
187
+ paired_at = excluded.paired_at,
188
+ device_name = excluded.device_name,
189
+ platform = excluded.platform,
190
+ system = excluded.system,
191
+ last_boot_at = excluded.last_boot_at,
192
+ last_online_at = excluded.last_online_at,
193
+ last_offline_at = excluded.last_offline_at,
194
+ last_policy_push_at = excluded.last_policy_push_at
195
+ `).run(deviceId, record.pairingToken, record.pairedAt, record.deviceName ?? null, record.platform ?? null, record.system ?? null, record.lastBootAt ?? null, record.lastOnlineAt ?? null, record.lastOfflineAt ?? null, record.lastPolicyPushAt ?? null);
196
+ }
197
+ async getPairedDevice(deviceId) {
198
+ const row = this.db.prepare(`
199
+ SELECT *
200
+ FROM paired_devices
201
+ WHERE device_id = ?
202
+ `).get(deviceId);
203
+ return row ? this.mapPairedDeviceRow(row) : undefined;
204
+ }
205
+ async deletePairedDevice(deviceId) {
206
+ this.db.prepare("DELETE FROM paired_devices WHERE device_id = ?").run(deviceId);
207
+ }
208
+ async listPairedDevices() {
209
+ const rows = this.db.prepare(`
210
+ SELECT *
211
+ FROM paired_devices
212
+ ORDER BY paired_at ASC
213
+ `).all();
214
+ return rows.map((row) => ({
215
+ deviceId: row.device_id,
216
+ record: this.mapPairedDeviceRow(row)
217
+ }));
218
+ }
219
+ async listPlaylists() {
220
+ const rows = this.db.prepare(`
221
+ SELECT *
222
+ FROM playlists
223
+ ORDER BY updated_at DESC
224
+ `).all();
225
+ return rows.map((row) => this.mapPlaylistRow(row));
226
+ }
227
+ async getPlaylist(id) {
228
+ const row = this.db.prepare(`
229
+ SELECT *
230
+ FROM playlists
231
+ WHERE id = ?
232
+ `).get(id);
233
+ return row ? this.mapPlaylistRow(row) : undefined;
234
+ }
235
+ async setPlaylist(record) {
236
+ this.db.prepare(`
237
+ INSERT INTO playlists (
238
+ id,
239
+ name,
240
+ schedule_json,
241
+ items_json,
242
+ version,
243
+ updated_at,
244
+ retired,
245
+ retired_at
246
+ )
247
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
248
+ ON CONFLICT(id) DO UPDATE SET
249
+ name = excluded.name,
250
+ schedule_json = excluded.schedule_json,
251
+ items_json = excluded.items_json,
252
+ version = excluded.version,
253
+ updated_at = excluded.updated_at,
254
+ retired = excluded.retired,
255
+ retired_at = excluded.retired_at
256
+ `).run(record.id, record.name, record.schedule ? JSON.stringify(record.schedule) : null, JSON.stringify(record.items ?? []), record.version, record.updatedAt, record.retired ? 1 : 0, record.retiredAt ?? null);
257
+ }
258
+ async isPlaylistNameTaken(name, excludeId) {
259
+ const target = name.trim().toLowerCase();
260
+ if (!target)
261
+ return false;
262
+ const row = this.db.prepare(`
263
+ SELECT id
264
+ FROM playlists
265
+ WHERE retired = 0
266
+ AND lower(trim(name)) = ?
267
+ AND (? IS NULL OR id != ?)
268
+ LIMIT 1
269
+ `).get(target, excludeId ?? null, excludeId ?? null);
270
+ return !!row;
271
+ }
272
+ async getDeviceAssignments(deviceId) {
273
+ const rows = this.db.prepare(`
274
+ SELECT playlist_id, published_version, published_at, snapshot_json
275
+ FROM device_assignments
276
+ WHERE device_id = ?
277
+ ORDER BY sort_order ASC
278
+ `).all(deviceId);
279
+ return rows.map((row) => ({
280
+ playlistId: row.playlist_id,
281
+ publishedVersion: row.published_version,
282
+ publishedAt: row.published_at,
283
+ snapshot: parseJson(row.snapshot_json, {
284
+ id: row.playlist_id,
285
+ name: row.playlist_id,
286
+ version: row.published_version,
287
+ items: []
288
+ })
289
+ }));
290
+ }
291
+ async setDeviceAssignments(deviceId, assignments) {
292
+ const replaceAssignments = this.db.transaction(() => {
293
+ this.db.prepare("DELETE FROM device_assignments WHERE device_id = ?").run(deviceId);
294
+ const insert = this.db.prepare(`
295
+ INSERT INTO device_assignments (
296
+ device_id,
297
+ playlist_id,
298
+ sort_order,
299
+ published_version,
300
+ published_at,
301
+ snapshot_json
302
+ )
303
+ VALUES (?, ?, ?, ?, ?, ?)
304
+ `);
305
+ assignments.forEach((assignment, index) => {
306
+ insert.run(deviceId, assignment.playlistId, index, assignment.publishedVersion, assignment.publishedAt, JSON.stringify(assignment.snapshot));
307
+ });
308
+ });
309
+ replaceAssignments();
310
+ }
311
+ mapDeviceRegistryRow(row) {
312
+ return {
313
+ permanentPairingCode: row.permanent_pairing_code,
314
+ codeCreatedAt: row.code_created_at,
315
+ serialNumber: optionalString(row.serial_number),
316
+ firstSeenAt: optionalNumber(row.first_seen_at),
317
+ lastHelloAt: optionalNumber(row.last_hello_at)
318
+ };
319
+ }
320
+ mapPairedDeviceRow(row) {
321
+ return {
322
+ pairingToken: row.pairing_token,
323
+ pairedAt: row.paired_at,
324
+ deviceName: optionalString(row.device_name),
325
+ platform: optionalString(row.platform),
326
+ system: optionalString(row.system),
327
+ lastBootAt: optionalString(row.last_boot_at),
328
+ lastOnlineAt: optionalString(row.last_online_at),
329
+ lastOfflineAt: optionalString(row.last_offline_at),
330
+ lastPolicyPushAt: optionalString(row.last_policy_push_at)
331
+ };
332
+ }
333
+ mapPlaylistRow(row) {
334
+ return {
335
+ id: row.id,
336
+ name: row.name,
337
+ schedule: parseJson(row.schedule_json, undefined),
338
+ items: parseJson(row.items_json, []),
339
+ version: row.version,
340
+ updatedAt: row.updated_at,
341
+ retired: row.retired === 1,
342
+ retiredAt: optionalString(row.retired_at)
343
+ };
344
+ }
345
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomorrowos/sdk",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "description": "TomorrowOS CMS server SDK - WebSocket transport, pairing, device commands, optional static CMS UI. Includes CLI (tomorrowos init / build) and starter templates.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -32,13 +32,14 @@
32
32
  "node": ">=18"
33
33
  },
34
34
  "dependencies": {
35
+ "better-sqlite3": "^12.11.1",
35
36
  "ws": "^8.18.0"
36
37
  },
37
38
  "devDependencies": {
39
+ "@types/better-sqlite3": "^7.6.13",
38
40
  "@types/node": "^20.19.41",
39
41
  "@types/ws": "^8.5.10",
40
42
  "typescript": "^5.5.0"
41
43
  },
42
44
  "license": "Apache-2.0"
43
45
  }
44
-
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "my-cms",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "description": "CMS server on @tomorrowos/sdk. Add your UI (React, static files, etc.) alongside this server.",
5
5
  "private": true,
6
6
  "type": "module",
@@ -10,7 +10,7 @@
10
10
  "build-player": "tomorrowos build --platform tizen"
11
11
  },
12
12
  "dependencies": {
13
- "@tomorrowos/sdk": "^0.4.7"
13
+ "@tomorrowos/sdk": "^0.4.8"
14
14
  },
15
15
  "devDependencies": {
16
16
  "@types/node": "^20.0.0",
@@ -1,22 +1,25 @@
1
1
  /**
2
2
  * TomorrowOS CMS server — minimal starter.
3
- * Add routes, database, or serve a React build via listen({ staticRoot }).
3
+ * Uses SQLite by default so pairings/playlists survive server restarts.
4
4
  */
5
5
 
6
6
  import { readFileSync } from "fs";
7
7
  import { fileURLToPath } from "url";
8
8
  import { dirname, join } from "path";
9
- import { TomorrowOS } from "@tomorrowos/sdk";
9
+ import { createTomorrowOSStore, TomorrowOS } from "@tomorrowos/sdk";
10
10
 
11
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
12
  const brand = JSON.parse(readFileSync(join(__dirname, "brand.json"), "utf8"));
13
+ const store = createTomorrowOSStore({
14
+ sqlitePath: join(__dirname, "data", "tomorrowos.db")
15
+ });
13
16
 
14
- const tomorrowos = new TomorrowOS({ brand });
17
+ const tomorrowos = new TomorrowOS({ brand, store });
15
18
 
16
19
  tomorrowos.listen({
17
20
  port: Number(process.env.PORT) || 3000,
18
21
  host: "0.0.0.0",
19
- staticRoot: join(__dirname,"public"),
22
+ staticRoot: join(__dirname, "public"),
20
23
  });
21
24
 
22
25
  tomorrowos.on("device.paired", (event) => {