@tomorrowos/sdk 0.4.7 → 0.5.0
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 +30 -0
- package/dist/cli.js +122 -2
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/store/factory.d.ts +20 -0
- package/dist/store/factory.d.ts.map +1 -0
- package/dist/store/factory.js +44 -0
- package/dist/store/migration.d.ts +12 -0
- package/dist/store/migration.d.ts.map +1 -0
- package/dist/store/migration.js +47 -0
- package/dist/store/postgres-store.d.ts +43 -0
- package/dist/store/postgres-store.d.ts.map +1 -0
- package/dist/store/postgres-store.js +435 -0
- package/dist/store/sqlite-store.d.ts +41 -0
- package/dist/store/sqlite-store.d.ts.map +1 -0
- package/dist/store/sqlite-store.js +367 -0
- package/dist/store/types.d.ts +22 -0
- package/dist/store/types.d.ts.map +1 -1
- package/package.json +5 -2
- package/templates/cms-starter/.env.example +10 -0
- package/templates/cms-starter/package.json +3 -2
- package/templates/cms-starter/server.ts +8 -4
- package/templates/cms-starter/template/databaseInterface/README.md +14 -0
- package/templates/cms-starter/template/databaseInterface/sqlite-interface.ts +10 -0
- package/templates/cms-starter/template/databaseInterface/supabase-interface.ts +17 -0
|
@@ -0,0 +1,367 @@
|
|
|
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 listPendingCodes() {
|
|
131
|
+
const rows = this.db.prepare(`
|
|
132
|
+
SELECT code, device_id, created_at
|
|
133
|
+
FROM pending_codes
|
|
134
|
+
ORDER BY created_at ASC
|
|
135
|
+
`).all();
|
|
136
|
+
return rows.map((row) => ({
|
|
137
|
+
code: row.code,
|
|
138
|
+
record: { deviceId: row.device_id, createdAt: row.created_at }
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
async getDeviceRegistry(deviceId) {
|
|
142
|
+
const row = this.db.prepare(`
|
|
143
|
+
SELECT *
|
|
144
|
+
FROM device_registry
|
|
145
|
+
WHERE device_id = ?
|
|
146
|
+
`).get(deviceId);
|
|
147
|
+
return row ? this.mapDeviceRegistryRow(row) : undefined;
|
|
148
|
+
}
|
|
149
|
+
async setDeviceRegistry(deviceId, record) {
|
|
150
|
+
this.db.prepare(`
|
|
151
|
+
INSERT INTO device_registry (
|
|
152
|
+
device_id,
|
|
153
|
+
permanent_pairing_code,
|
|
154
|
+
code_created_at,
|
|
155
|
+
serial_number,
|
|
156
|
+
first_seen_at,
|
|
157
|
+
last_hello_at
|
|
158
|
+
)
|
|
159
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
160
|
+
ON CONFLICT(device_id) DO UPDATE SET
|
|
161
|
+
permanent_pairing_code = excluded.permanent_pairing_code,
|
|
162
|
+
code_created_at = excluded.code_created_at,
|
|
163
|
+
serial_number = excluded.serial_number,
|
|
164
|
+
first_seen_at = excluded.first_seen_at,
|
|
165
|
+
last_hello_at = excluded.last_hello_at
|
|
166
|
+
`).run(deviceId, record.permanentPairingCode, record.codeCreatedAt, record.serialNumber ?? null, record.firstSeenAt ?? null, record.lastHelloAt ?? null);
|
|
167
|
+
}
|
|
168
|
+
async getDeviceRegistryByCode(code) {
|
|
169
|
+
const row = this.db.prepare(`
|
|
170
|
+
SELECT *
|
|
171
|
+
FROM device_registry
|
|
172
|
+
WHERE permanent_pairing_code = ?
|
|
173
|
+
`).get(code);
|
|
174
|
+
if (!row)
|
|
175
|
+
return undefined;
|
|
176
|
+
return {
|
|
177
|
+
deviceId: row.device_id,
|
|
178
|
+
record: this.mapDeviceRegistryRow(row)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
async listDeviceRegistry() {
|
|
182
|
+
const rows = this.db.prepare(`
|
|
183
|
+
SELECT *
|
|
184
|
+
FROM device_registry
|
|
185
|
+
ORDER BY code_created_at ASC
|
|
186
|
+
`).all();
|
|
187
|
+
return rows.map((row) => ({
|
|
188
|
+
deviceId: row.device_id,
|
|
189
|
+
record: this.mapDeviceRegistryRow(row)
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
async setPairedDevice(deviceId, record) {
|
|
193
|
+
this.db.prepare(`
|
|
194
|
+
INSERT INTO paired_devices (
|
|
195
|
+
device_id,
|
|
196
|
+
pairing_token,
|
|
197
|
+
paired_at,
|
|
198
|
+
device_name,
|
|
199
|
+
platform,
|
|
200
|
+
system,
|
|
201
|
+
last_boot_at,
|
|
202
|
+
last_online_at,
|
|
203
|
+
last_offline_at,
|
|
204
|
+
last_policy_push_at
|
|
205
|
+
)
|
|
206
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
207
|
+
ON CONFLICT(device_id) DO UPDATE SET
|
|
208
|
+
pairing_token = excluded.pairing_token,
|
|
209
|
+
paired_at = excluded.paired_at,
|
|
210
|
+
device_name = excluded.device_name,
|
|
211
|
+
platform = excluded.platform,
|
|
212
|
+
system = excluded.system,
|
|
213
|
+
last_boot_at = excluded.last_boot_at,
|
|
214
|
+
last_online_at = excluded.last_online_at,
|
|
215
|
+
last_offline_at = excluded.last_offline_at,
|
|
216
|
+
last_policy_push_at = excluded.last_policy_push_at
|
|
217
|
+
`).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);
|
|
218
|
+
}
|
|
219
|
+
async getPairedDevice(deviceId) {
|
|
220
|
+
const row = this.db.prepare(`
|
|
221
|
+
SELECT *
|
|
222
|
+
FROM paired_devices
|
|
223
|
+
WHERE device_id = ?
|
|
224
|
+
`).get(deviceId);
|
|
225
|
+
return row ? this.mapPairedDeviceRow(row) : undefined;
|
|
226
|
+
}
|
|
227
|
+
async deletePairedDevice(deviceId) {
|
|
228
|
+
this.db.prepare("DELETE FROM paired_devices WHERE device_id = ?").run(deviceId);
|
|
229
|
+
}
|
|
230
|
+
async listPairedDevices() {
|
|
231
|
+
const rows = this.db.prepare(`
|
|
232
|
+
SELECT *
|
|
233
|
+
FROM paired_devices
|
|
234
|
+
ORDER BY paired_at ASC
|
|
235
|
+
`).all();
|
|
236
|
+
return rows.map((row) => ({
|
|
237
|
+
deviceId: row.device_id,
|
|
238
|
+
record: this.mapPairedDeviceRow(row)
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
async listPlaylists() {
|
|
242
|
+
const rows = this.db.prepare(`
|
|
243
|
+
SELECT *
|
|
244
|
+
FROM playlists
|
|
245
|
+
ORDER BY updated_at DESC
|
|
246
|
+
`).all();
|
|
247
|
+
return rows.map((row) => this.mapPlaylistRow(row));
|
|
248
|
+
}
|
|
249
|
+
async getPlaylist(id) {
|
|
250
|
+
const row = this.db.prepare(`
|
|
251
|
+
SELECT *
|
|
252
|
+
FROM playlists
|
|
253
|
+
WHERE id = ?
|
|
254
|
+
`).get(id);
|
|
255
|
+
return row ? this.mapPlaylistRow(row) : undefined;
|
|
256
|
+
}
|
|
257
|
+
async setPlaylist(record) {
|
|
258
|
+
this.db.prepare(`
|
|
259
|
+
INSERT INTO playlists (
|
|
260
|
+
id,
|
|
261
|
+
name,
|
|
262
|
+
schedule_json,
|
|
263
|
+
items_json,
|
|
264
|
+
version,
|
|
265
|
+
updated_at,
|
|
266
|
+
retired,
|
|
267
|
+
retired_at
|
|
268
|
+
)
|
|
269
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
270
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
271
|
+
name = excluded.name,
|
|
272
|
+
schedule_json = excluded.schedule_json,
|
|
273
|
+
items_json = excluded.items_json,
|
|
274
|
+
version = excluded.version,
|
|
275
|
+
updated_at = excluded.updated_at,
|
|
276
|
+
retired = excluded.retired,
|
|
277
|
+
retired_at = excluded.retired_at
|
|
278
|
+
`).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);
|
|
279
|
+
}
|
|
280
|
+
async isPlaylistNameTaken(name, excludeId) {
|
|
281
|
+
const target = name.trim().toLowerCase();
|
|
282
|
+
if (!target)
|
|
283
|
+
return false;
|
|
284
|
+
const row = this.db.prepare(`
|
|
285
|
+
SELECT id
|
|
286
|
+
FROM playlists
|
|
287
|
+
WHERE retired = 0
|
|
288
|
+
AND lower(trim(name)) = ?
|
|
289
|
+
AND (? IS NULL OR id != ?)
|
|
290
|
+
LIMIT 1
|
|
291
|
+
`).get(target, excludeId ?? null, excludeId ?? null);
|
|
292
|
+
return !!row;
|
|
293
|
+
}
|
|
294
|
+
async getDeviceAssignments(deviceId) {
|
|
295
|
+
const rows = this.db.prepare(`
|
|
296
|
+
SELECT playlist_id, published_version, published_at, snapshot_json
|
|
297
|
+
FROM device_assignments
|
|
298
|
+
WHERE device_id = ?
|
|
299
|
+
ORDER BY sort_order ASC
|
|
300
|
+
`).all(deviceId);
|
|
301
|
+
return rows.map((row) => ({
|
|
302
|
+
playlistId: row.playlist_id,
|
|
303
|
+
publishedVersion: row.published_version,
|
|
304
|
+
publishedAt: row.published_at,
|
|
305
|
+
snapshot: parseJson(row.snapshot_json, {
|
|
306
|
+
id: row.playlist_id,
|
|
307
|
+
name: row.playlist_id,
|
|
308
|
+
version: row.published_version,
|
|
309
|
+
items: []
|
|
310
|
+
})
|
|
311
|
+
}));
|
|
312
|
+
}
|
|
313
|
+
async setDeviceAssignments(deviceId, assignments) {
|
|
314
|
+
const replaceAssignments = this.db.transaction(() => {
|
|
315
|
+
this.db.prepare("DELETE FROM device_assignments WHERE device_id = ?").run(deviceId);
|
|
316
|
+
const insert = this.db.prepare(`
|
|
317
|
+
INSERT INTO device_assignments (
|
|
318
|
+
device_id,
|
|
319
|
+
playlist_id,
|
|
320
|
+
sort_order,
|
|
321
|
+
published_version,
|
|
322
|
+
published_at,
|
|
323
|
+
snapshot_json
|
|
324
|
+
)
|
|
325
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
326
|
+
`);
|
|
327
|
+
assignments.forEach((assignment, index) => {
|
|
328
|
+
insert.run(deviceId, assignment.playlistId, index, assignment.publishedVersion, assignment.publishedAt, JSON.stringify(assignment.snapshot));
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
replaceAssignments();
|
|
332
|
+
}
|
|
333
|
+
mapDeviceRegistryRow(row) {
|
|
334
|
+
return {
|
|
335
|
+
permanentPairingCode: row.permanent_pairing_code,
|
|
336
|
+
codeCreatedAt: row.code_created_at,
|
|
337
|
+
serialNumber: optionalString(row.serial_number),
|
|
338
|
+
firstSeenAt: optionalNumber(row.first_seen_at),
|
|
339
|
+
lastHelloAt: optionalNumber(row.last_hello_at)
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
mapPairedDeviceRow(row) {
|
|
343
|
+
return {
|
|
344
|
+
pairingToken: row.pairing_token,
|
|
345
|
+
pairedAt: row.paired_at,
|
|
346
|
+
deviceName: optionalString(row.device_name),
|
|
347
|
+
platform: optionalString(row.platform),
|
|
348
|
+
system: optionalString(row.system),
|
|
349
|
+
lastBootAt: optionalString(row.last_boot_at),
|
|
350
|
+
lastOnlineAt: optionalString(row.last_online_at),
|
|
351
|
+
lastOfflineAt: optionalString(row.last_offline_at),
|
|
352
|
+
lastPolicyPushAt: optionalString(row.last_policy_push_at)
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
mapPlaylistRow(row) {
|
|
356
|
+
return {
|
|
357
|
+
id: row.id,
|
|
358
|
+
name: row.name,
|
|
359
|
+
schedule: parseJson(row.schedule_json, undefined),
|
|
360
|
+
items: parseJson(row.items_json, []),
|
|
361
|
+
version: row.version,
|
|
362
|
+
updatedAt: row.updated_at,
|
|
363
|
+
retired: row.retired === 1,
|
|
364
|
+
retiredAt: optionalString(row.retired_at)
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
}
|
package/dist/store/types.d.ts
CHANGED
|
@@ -29,6 +29,14 @@ export interface PairedDeviceEntry {
|
|
|
29
29
|
deviceId: string;
|
|
30
30
|
record: PairedDeviceRecord;
|
|
31
31
|
}
|
|
32
|
+
export interface PendingCodeEntry {
|
|
33
|
+
code: string;
|
|
34
|
+
record: PendingCodeRecord;
|
|
35
|
+
}
|
|
36
|
+
export interface DeviceRegistryEntry {
|
|
37
|
+
deviceId: string;
|
|
38
|
+
record: DeviceRegistryRecord;
|
|
39
|
+
}
|
|
32
40
|
export interface PlaylistItemRecord {
|
|
33
41
|
url: string;
|
|
34
42
|
type?: string;
|
|
@@ -93,4 +101,18 @@ export interface TomorrowOSStore {
|
|
|
93
101
|
getDeviceAssignments(deviceId: string): Promise<DevicePlaylistAssignment[]>;
|
|
94
102
|
setDeviceAssignments(deviceId: string, assignments: DevicePlaylistAssignment[]): Promise<void>;
|
|
95
103
|
}
|
|
104
|
+
export interface TomorrowOSDataSnapshot {
|
|
105
|
+
pendingCodes: PendingCodeEntry[];
|
|
106
|
+
deviceRegistry: DeviceRegistryEntry[];
|
|
107
|
+
pairedDevices: PairedDeviceEntry[];
|
|
108
|
+
playlists: StoredPlaylist[];
|
|
109
|
+
deviceAssignments: Array<{
|
|
110
|
+
deviceId: string;
|
|
111
|
+
assignments: DevicePlaylistAssignment[];
|
|
112
|
+
}>;
|
|
113
|
+
}
|
|
114
|
+
export interface TomorrowOSMigratableStore extends TomorrowOSStore {
|
|
115
|
+
listPendingCodes(): Promise<PendingCodeEntry[]>;
|
|
116
|
+
listDeviceRegistry(): Promise<DeviceRegistryEntry[]>;
|
|
117
|
+
}
|
|
96
118
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomorrowos/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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,16 @@
|
|
|
32
32
|
"node": ">=18"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
+
"better-sqlite3": "^12.11.1",
|
|
36
|
+
"pg": "^8.22.0",
|
|
35
37
|
"ws": "^8.18.0"
|
|
36
38
|
},
|
|
37
39
|
"devDependencies": {
|
|
40
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
38
41
|
"@types/node": "^20.19.41",
|
|
42
|
+
"@types/pg": "^8.20.0",
|
|
39
43
|
"@types/ws": "^8.5.10",
|
|
40
44
|
"typescript": "^5.5.0"
|
|
41
45
|
},
|
|
42
46
|
"license": "Apache-2.0"
|
|
43
47
|
}
|
|
44
|
-
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Default local database.
|
|
2
|
+
TOMORROWOS_STORE=sqlite
|
|
3
|
+
TOMORROWOS_DB_PATH=./data/tomorrowos.db
|
|
4
|
+
|
|
5
|
+
# Supabase/Postgres option.
|
|
6
|
+
# TOMORROWOS_STORE=supabase
|
|
7
|
+
# DATABASE_URL=postgresql://postgres:[PASSWORD]@[HOST]:5432/postgres
|
|
8
|
+
# DATABASE_SSL=true
|
|
9
|
+
|
|
10
|
+
PORT=3000
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "my-cms",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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,8 @@
|
|
|
10
10
|
"build-player": "tomorrowos build --platform tizen"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@tomorrowos/sdk": "^0.
|
|
13
|
+
"@tomorrowos/sdk": "^0.5.0",
|
|
14
|
+
"dotenv": "^17.2.3"
|
|
14
15
|
},
|
|
15
16
|
"devDependencies": {
|
|
16
17
|
"@types/node": "^20.0.0",
|
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TomorrowOS CMS server — minimal starter.
|
|
3
|
-
*
|
|
3
|
+
* Uses SQLite by default so pairings/playlists survive server restarts.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import "dotenv/config";
|
|
6
7
|
import { readFileSync } from "fs";
|
|
7
8
|
import { fileURLToPath } from "url";
|
|
8
9
|
import { dirname, join } from "path";
|
|
9
|
-
import { TomorrowOS } from "@tomorrowos/sdk";
|
|
10
|
+
import { createTomorrowOSStore, TomorrowOS } from "@tomorrowos/sdk";
|
|
10
11
|
|
|
11
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
const brand = JSON.parse(readFileSync(join(__dirname, "brand.json"), "utf8"));
|
|
14
|
+
const store = createTomorrowOSStore({
|
|
15
|
+
sqlitePath: join(__dirname, "data", "tomorrowos.db")
|
|
16
|
+
});
|
|
13
17
|
|
|
14
|
-
const tomorrowos = new TomorrowOS({ brand });
|
|
18
|
+
const tomorrowos = new TomorrowOS({ brand, store });
|
|
15
19
|
|
|
16
20
|
tomorrowos.listen({
|
|
17
21
|
port: Number(process.env.PORT) || 3000,
|
|
18
22
|
host: "0.0.0.0",
|
|
19
|
-
staticRoot: join(__dirname,"public"),
|
|
23
|
+
staticRoot: join(__dirname, "public"),
|
|
20
24
|
});
|
|
21
25
|
|
|
22
26
|
tomorrowos.on("device.paired", (event) => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Database Interface Examples
|
|
2
|
+
|
|
3
|
+
The starter server already switches databases from `.env`:
|
|
4
|
+
|
|
5
|
+
- `TOMORROWOS_STORE=sqlite` uses `data/tomorrowos.db`.
|
|
6
|
+
- `TOMORROWOS_STORE=supabase` plus `DATABASE_URL=...` uses Supabase/Postgres.
|
|
7
|
+
- `TOMORROWOS_STORE=memory` is only for throwaway tests.
|
|
8
|
+
|
|
9
|
+
These files are reference examples for customers who want to understand or build
|
|
10
|
+
their own database interface. You do not need to import them for the starter to
|
|
11
|
+
work.
|
|
12
|
+
|
|
13
|
+
If you build a custom database backend, implement `TomorrowOSStore` and pass it
|
|
14
|
+
to `new TomorrowOS({ brand, store })` in `server.ts`.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference only. The starter already uses this behavior by default.
|
|
3
|
+
*/
|
|
4
|
+
import { SQLiteStore } from "@tomorrowos/sdk";
|
|
5
|
+
|
|
6
|
+
export function createSQLiteInterface() {
|
|
7
|
+
return new SQLiteStore({
|
|
8
|
+
databasePath: process.env.TOMORROWOS_DB_PATH || "./data/tomorrowos.db"
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference only. Supabase exposes a Postgres connection string, so TomorrowOS
|
|
3
|
+
* uses PostgresStore for Supabase-backed persistence.
|
|
4
|
+
*/
|
|
5
|
+
import { PostgresStore } from "@tomorrowos/sdk";
|
|
6
|
+
|
|
7
|
+
export function createSupabaseInterface() {
|
|
8
|
+
const connectionString = process.env.DATABASE_URL;
|
|
9
|
+
if (!connectionString) {
|
|
10
|
+
throw new Error("DATABASE_URL is required for Supabase/Postgres.");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return new PostgresStore({
|
|
14
|
+
connectionString,
|
|
15
|
+
ssl: { rejectUnauthorized: false }
|
|
16
|
+
});
|
|
17
|
+
}
|