@tomorrowos/sdk 0.4.8 → 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 +25 -4
- package/dist/cli.js +113 -2
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/store/factory.d.ts +7 -7
- package/dist/store/factory.d.ts.map +1 -1
- package/dist/store/factory.js +29 -8
- 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 +4 -2
- package/dist/store/sqlite-store.d.ts.map +1 -1
- package/dist/store/sqlite-store.js +22 -0
- package/dist/store/types.d.ts +22 -0
- package/dist/store/types.d.ts.map +1 -1
- package/package.json +3 -1
- package/templates/cms-starter/.env.example +10 -0
- package/templates/cms-starter/package.json +3 -2
- package/templates/cms-starter/server.ts +1 -0
- 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
package/README.md
CHANGED
|
@@ -42,10 +42,31 @@ npm run dev
|
|
|
42
42
|
schema. The starter server uses that SQLite database by default, so pairings,
|
|
43
43
|
playlists, and device assignments survive normal server restarts.
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
To switch to Supabase/Postgres, edit `.env`:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
TOMORROWOS_STORE=supabase
|
|
49
|
+
DATABASE_URL=postgresql://...
|
|
50
|
+
DATABASE_SSL=true
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
For throwaway demos/tests, set `TOMORROWOS_STORE=memory`. The generated starter
|
|
54
|
+
also includes `template/databaseInterface/` with SQLite and Supabase examples
|
|
55
|
+
for customers who want to build their own `TomorrowOSStore` implementation.
|
|
56
|
+
|
|
57
|
+
Migrate database records between supported stores:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx tomorrowos migrate \
|
|
61
|
+
--from sqlite \
|
|
62
|
+
--from-sqlite ./data/tomorrowos.db \
|
|
63
|
+
--to supabase \
|
|
64
|
+
--to-database-url "$DATABASE_URL"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The migrate command supports `sqlite`, `postgres`, and `supabase` in either
|
|
68
|
+
direction. It migrates TomorrowOS database records only; copy `public/uploads`
|
|
69
|
+
or object-storage media separately.
|
|
49
70
|
|
|
50
71
|
Build a player (placeholder in current SDK — see `PLAYER_INSTALL.md` for platform tooling):
|
|
51
72
|
|
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
|
+
import { migrateTomorrowOSData } from "./store/migration.js";
|
|
6
|
+
import { PostgresStore } from "./store/postgres-store.js";
|
|
5
7
|
import { SQLiteStore } from "./store/sqlite-store.js";
|
|
6
8
|
function packageRoot() {
|
|
7
9
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -89,11 +91,93 @@ function cmdBuild(argv) {
|
|
|
89
91
|
console.log("Use your TomorrowOS player repository (assemble Web app + config), then run the platform SDK (e.g. Tizen CLI tizen package) from the build output directory.");
|
|
90
92
|
process.exitCode = 0;
|
|
91
93
|
}
|
|
94
|
+
function optionValue(argv, name) {
|
|
95
|
+
const withEquals = `${name}=`;
|
|
96
|
+
const found = argv.find((arg) => arg.startsWith(withEquals));
|
|
97
|
+
if (found)
|
|
98
|
+
return found.slice(withEquals.length);
|
|
99
|
+
const idx = argv.indexOf(name);
|
|
100
|
+
if (idx >= 0)
|
|
101
|
+
return argv[idx + 1];
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
function normalizeDriver(raw, side) {
|
|
105
|
+
const driver = String(raw || "").trim().toLowerCase();
|
|
106
|
+
if (driver === "sqlite" || driver === "postgres" || driver === "supabase") {
|
|
107
|
+
return driver;
|
|
108
|
+
}
|
|
109
|
+
throw new Error(`--${side} must be sqlite, postgres, or supabase.`);
|
|
110
|
+
}
|
|
111
|
+
function resolvePostgresSsl(raw, databaseUrl) {
|
|
112
|
+
const value = raw?.trim().toLowerCase();
|
|
113
|
+
if (value === "false" || value === "0" || value === "off")
|
|
114
|
+
return false;
|
|
115
|
+
if (value === "true" || value === "1" || value === "on") {
|
|
116
|
+
return { rejectUnauthorized: false };
|
|
117
|
+
}
|
|
118
|
+
if (/supabase\.(co|com)|sslmode=require/i.test(databaseUrl)) {
|
|
119
|
+
return { rejectUnauthorized: false };
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
function createMigrationStore(argv, side) {
|
|
124
|
+
const driver = normalizeDriver(optionValue(argv, `--${side}`), side);
|
|
125
|
+
if (driver === "sqlite") {
|
|
126
|
+
const dbPath = optionValue(argv, `--${side}-sqlite`) ??
|
|
127
|
+
optionValue(argv, `--${side}-sqlite-path`);
|
|
128
|
+
if (!dbPath) {
|
|
129
|
+
throw new Error(`--${side}=sqlite requires --${side}-sqlite ./data/tomorrowos.db`);
|
|
130
|
+
}
|
|
131
|
+
const store = new SQLiteStore(dbPath);
|
|
132
|
+
return {
|
|
133
|
+
label: `sqlite:${path.resolve(dbPath)}`,
|
|
134
|
+
store,
|
|
135
|
+
close: async () => store.close()
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const databaseUrl = optionValue(argv, `--${side}-database-url`) ??
|
|
139
|
+
optionValue(argv, `--${side}-url`);
|
|
140
|
+
if (!databaseUrl) {
|
|
141
|
+
throw new Error(`--${side}=${driver} requires --${side}-database-url postgresql://...`);
|
|
142
|
+
}
|
|
143
|
+
const store = new PostgresStore({
|
|
144
|
+
connectionString: databaseUrl,
|
|
145
|
+
ssl: resolvePostgresSsl(optionValue(argv, `--${side}-database-ssl`), databaseUrl)
|
|
146
|
+
});
|
|
147
|
+
return {
|
|
148
|
+
label: `${driver}:DATABASE_URL`,
|
|
149
|
+
store,
|
|
150
|
+
close: async () => store.close()
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
async function cmdMigrate(argv) {
|
|
154
|
+
if (argv.includes("-h") || argv.includes("--help")) {
|
|
155
|
+
printMigrateHelp();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const from = createMigrationStore(argv, "from");
|
|
159
|
+
const to = createMigrationStore(argv, "to");
|
|
160
|
+
try {
|
|
161
|
+
console.log(`[tomorrowos] Migrating data from ${from.label} to ${to.label}`);
|
|
162
|
+
const result = await migrateTomorrowOSData(from.store, to.store);
|
|
163
|
+
console.log("[tomorrowos] Migration complete:");
|
|
164
|
+
console.log(` pending codes: ${result.pendingCodes}`);
|
|
165
|
+
console.log(` device registry records: ${result.deviceRegistry}`);
|
|
166
|
+
console.log(` paired devices: ${result.pairedDevices}`);
|
|
167
|
+
console.log(` playlists: ${result.playlists}`);
|
|
168
|
+
console.log(` device assignments: ${result.deviceAssignments}`);
|
|
169
|
+
console.log("[tomorrowos] Note: media files in public/uploads or object storage are not copied by database migration.");
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
await Promise.allSettled([from.close(), to.close()]);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
92
175
|
function printHelp() {
|
|
93
176
|
console.log(`tomorrowos — TomorrowOS SDK CLI
|
|
94
177
|
|
|
95
178
|
Usage:
|
|
96
179
|
tomorrowos init [directory] Copy cms-starter template (default: my-tomorrowos-cms)
|
|
180
|
+
tomorrowos migrate [options] Migrate TomorrowOS data between supported databases
|
|
97
181
|
tomorrowos build --platform … Placeholder; player packaging lives in the player repo
|
|
98
182
|
|
|
99
183
|
Options:
|
|
@@ -102,9 +186,29 @@ Options:
|
|
|
102
186
|
Examples:
|
|
103
187
|
npx @tomorrowos/sdk init
|
|
104
188
|
npx @tomorrowos/sdk init ./my-cms
|
|
189
|
+
npx @tomorrowos/sdk migrate --from sqlite --from-sqlite ./data/tomorrowos.db --to supabase --to-database-url "$DATABASE_URL"
|
|
105
190
|
`);
|
|
106
191
|
}
|
|
107
|
-
function
|
|
192
|
+
function printMigrateHelp() {
|
|
193
|
+
console.log(`tomorrowos migrate — migrate TomorrowOS SDK data
|
|
194
|
+
|
|
195
|
+
Supported stores:
|
|
196
|
+
sqlite
|
|
197
|
+
postgres
|
|
198
|
+
supabase
|
|
199
|
+
|
|
200
|
+
Examples:
|
|
201
|
+
tomorrowos migrate --from sqlite --from-sqlite ./data/tomorrowos.db --to supabase --to-database-url "$DATABASE_URL"
|
|
202
|
+
tomorrowos migrate --from supabase --from-database-url "$DATABASE_URL" --to sqlite --to-sqlite ./data/tomorrowos.db
|
|
203
|
+
tomorrowos migrate --from postgres --from-database-url "$OLD_DATABASE_URL" --to postgres --to-database-url "$NEW_DATABASE_URL"
|
|
204
|
+
|
|
205
|
+
Notes:
|
|
206
|
+
- Migrates pairing/device/playlist/assignment database records.
|
|
207
|
+
- Does not copy media files from public/uploads or object storage.
|
|
208
|
+
- Target records with the same primary keys are overwritten/updated.
|
|
209
|
+
`);
|
|
210
|
+
}
|
|
211
|
+
async function main() {
|
|
108
212
|
const argv = process.argv.slice(2);
|
|
109
213
|
if (argv.length === 0 || argv[0] === "-h" || argv[0] === "--help") {
|
|
110
214
|
printHelp();
|
|
@@ -123,8 +227,15 @@ function main() {
|
|
|
123
227
|
cmdBuild(rest);
|
|
124
228
|
return;
|
|
125
229
|
}
|
|
230
|
+
if (cmd === "migrate") {
|
|
231
|
+
await cmdMigrate(rest);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
126
234
|
console.error(`[tomorrowos] Unknown command: ${cmd}`);
|
|
127
235
|
printHelp();
|
|
128
236
|
process.exit(1);
|
|
129
237
|
}
|
|
130
|
-
main()
|
|
238
|
+
main().catch((err) => {
|
|
239
|
+
console.error("[tomorrowos]", err instanceof Error ? err.message : err);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
export { TomorrowOS } from "./tomorrowos.js";
|
|
2
2
|
export type { DeviceListItem, ListenOptions, TomorrowOSBrand, TomorrowOSOptions } from "./tomorrowos.js";
|
|
3
|
-
export type { DevicePlaylistAssignment, DeviceRegistryRecord, PairedDeviceRecord, PendingCodeRecord, PlaylistItemRecord, PlaylistSchedule, PublishedPlaylistSnapshot, StoredPlaylist, TomorrowOSStore } from "./store/types.js";
|
|
3
|
+
export type { DevicePlaylistAssignment, DeviceRegistryRecord, PairedDeviceRecord, PendingCodeEntry, PendingCodeRecord, DeviceRegistryEntry, PlaylistItemRecord, PlaylistSchedule, PublishedPlaylistSnapshot, StoredPlaylist, TomorrowOSDataSnapshot, TomorrowOSMigratableStore, TomorrowOSStore } from "./store/types.js";
|
|
4
4
|
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 { PostgresStore } from "./store/postgres-store.js";
|
|
9
|
+
export type { PostgresStoreOptions } from "./store/postgres-store.js";
|
|
8
10
|
export { SQLiteStore } from "./store/sqlite-store.js";
|
|
9
11
|
export { createTomorrowOSStore, defaultSQLitePath } from "./store/factory.js";
|
|
10
12
|
export type { CreateTomorrowOSStoreOptions, TomorrowOSStoreDriver } from "./store/factory.js";
|
|
13
|
+
export { exportTomorrowOSData, importTomorrowOSData, migrateTomorrowOSData } from "./store/migration.js";
|
|
14
|
+
export type { MigrationResult } from "./store/migration.js";
|
|
11
15
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;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"}
|
|
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,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,yBAAyB,EACzB,cAAc,EACd,sBAAsB,EACtB,yBAAyB,EACzB,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,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,YAAY,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACtE,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;AAC5B,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -2,5 +2,7 @@ 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 { PostgresStore } from "./store/postgres-store.js";
|
|
5
6
|
export { SQLiteStore } from "./store/sqlite-store.js";
|
|
6
7
|
export { createTomorrowOSStore, defaultSQLitePath } from "./store/factory.js";
|
|
8
|
+
export { exportTomorrowOSData, importTomorrowOSData, migrateTomorrowOSData } from "./store/migration.js";
|
package/dist/store/factory.d.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
+
import type { PoolConfig } from "pg";
|
|
1
2
|
import type { TomorrowOSStore } from "./types.js";
|
|
2
|
-
export type TomorrowOSStoreDriver = "sqlite" | "memory";
|
|
3
|
+
export type TomorrowOSStoreDriver = "sqlite" | "postgres" | "supabase" | "memory";
|
|
3
4
|
export interface CreateTomorrowOSStoreOptions {
|
|
4
5
|
/**
|
|
5
|
-
* Defaults to TOMORROWOS_STORE, then
|
|
6
|
-
* Use memory only for tests or throwaway demos.
|
|
6
|
+
* Defaults to TOMORROWOS_STORE, then postgres when DATABASE_URL is set,
|
|
7
|
+
* otherwise sqlite. Use memory only for tests or throwaway demos.
|
|
7
8
|
*/
|
|
8
9
|
driver?: TomorrowOSStoreDriver;
|
|
9
10
|
/** Defaults to TOMORROWOS_DB_PATH, then ./data/tomorrowos.db. */
|
|
10
11
|
sqlitePath?: string;
|
|
11
|
-
/**
|
|
12
|
-
* Reserved for external database adapters. Supabase exposes Postgres URLs,
|
|
13
|
-
* which should be handled by a Postgres TomorrowOSStore implementation.
|
|
14
|
-
*/
|
|
12
|
+
/** Supabase/Postgres connection string. Defaults to DATABASE_URL. */
|
|
15
13
|
databaseUrl?: string;
|
|
14
|
+
/** Defaults from DATABASE_SSL / DATABASE_URL. */
|
|
15
|
+
postgresSsl?: PoolConfig["ssl"];
|
|
16
16
|
env?: NodeJS.ProcessEnv;
|
|
17
17
|
}
|
|
18
18
|
export declare function defaultSQLitePath(cwd?: string): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/store/factory.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/store/factory.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAKrC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,MAAM,qBAAqB,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,GAAG,QAAQ,CAAC;AAElF,MAAM,WAAW,4BAA4B;IAC3C;;;OAGG;IACH,MAAM,CAAC,EAAE,qBAAqB,CAAC;IAC/B,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iDAAiD;IACjD,WAAW,CAAC,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC;IAChC,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,CAmCjB"}
|
package/dist/store/factory.js
CHANGED
|
@@ -1,23 +1,44 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import { MemoryStore } from "./memory-store.js";
|
|
3
|
+
import { PostgresStore } from "./postgres-store.js";
|
|
3
4
|
import { SQLiteStore } from "./sqlite-store.js";
|
|
4
5
|
export function defaultSQLitePath(cwd = process.cwd()) {
|
|
5
6
|
return path.join(cwd, "data", "tomorrowos.db");
|
|
6
7
|
}
|
|
7
8
|
export function createTomorrowOSStore(options = {}) {
|
|
8
9
|
const env = options.env ?? process.env;
|
|
9
|
-
const driver = (options.driver ?? env.TOMORROWOS_STORE ?? "sqlite").toLowerCase();
|
|
10
10
|
const databaseUrl = options.databaseUrl ?? env.DATABASE_URL;
|
|
11
|
+
const driver = (options.driver ??
|
|
12
|
+
env.TOMORROWOS_STORE ??
|
|
13
|
+
(databaseUrl ? "postgres" : "sqlite")).toLowerCase();
|
|
11
14
|
if (driver === "memory") {
|
|
12
15
|
return new MemoryStore();
|
|
13
16
|
}
|
|
14
|
-
if (driver
|
|
15
|
-
|
|
17
|
+
if (driver === "postgres" || driver === "supabase") {
|
|
18
|
+
if (!databaseUrl) {
|
|
19
|
+
throw new Error(`TOMORROWOS_STORE=${driver} requires DATABASE_URL. Set DATABASE_URL in .env or unset TOMORROWOS_STORE to use SQLite.`);
|
|
20
|
+
}
|
|
21
|
+
return new PostgresStore({
|
|
22
|
+
connectionString: databaseUrl,
|
|
23
|
+
ssl: options.postgresSsl ?? resolvePostgresSsl(env, databaseUrl)
|
|
24
|
+
});
|
|
16
25
|
}
|
|
17
|
-
if (
|
|
18
|
-
|
|
26
|
+
if (driver === "sqlite") {
|
|
27
|
+
return new SQLiteStore({
|
|
28
|
+
databasePath: options.sqlitePath ?? env.TOMORROWOS_DB_PATH ?? defaultSQLitePath()
|
|
29
|
+
});
|
|
19
30
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
31
|
+
throw new Error(`Unsupported TOMORROWOS_STORE "${driver}". Use "sqlite", "supabase", "postgres", "memory", or pass a custom TomorrowOSStore to new TomorrowOS({ store }).`);
|
|
32
|
+
}
|
|
33
|
+
function resolvePostgresSsl(env, databaseUrl) {
|
|
34
|
+
const raw = env.DATABASE_SSL?.trim().toLowerCase();
|
|
35
|
+
if (raw === "false" || raw === "0" || raw === "off")
|
|
36
|
+
return false;
|
|
37
|
+
if (raw === "true" || raw === "1" || raw === "on") {
|
|
38
|
+
return { rejectUnauthorized: false };
|
|
39
|
+
}
|
|
40
|
+
if (/supabase\.(co|com)|sslmode=require/i.test(databaseUrl)) {
|
|
41
|
+
return { rejectUnauthorized: false };
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
23
44
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TomorrowOSDataSnapshot, TomorrowOSMigratableStore } from "./types.js";
|
|
2
|
+
export interface MigrationResult {
|
|
3
|
+
pendingCodes: number;
|
|
4
|
+
deviceRegistry: number;
|
|
5
|
+
pairedDevices: number;
|
|
6
|
+
playlists: number;
|
|
7
|
+
deviceAssignments: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function exportTomorrowOSData(store: TomorrowOSMigratableStore): Promise<TomorrowOSDataSnapshot>;
|
|
10
|
+
export declare function importTomorrowOSData(store: TomorrowOSMigratableStore, snapshot: TomorrowOSDataSnapshot): Promise<MigrationResult>;
|
|
11
|
+
export declare function migrateTomorrowOSData(from: TomorrowOSMigratableStore, to: TomorrowOSMigratableStore): Promise<MigrationResult>;
|
|
12
|
+
//# sourceMappingURL=migration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migration.d.ts","sourceRoot":"","sources":["../../src/store/migration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EACtB,yBAAyB,EAC1B,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,yBAAyB,GAC/B,OAAO,CAAC,sBAAsB,CAAC,CA2BjC;AAED,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,yBAAyB,EAChC,QAAQ,EAAE,sBAAsB,GAC/B,OAAO,CAAC,eAAe,CAAC,CA+B1B;AAED,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,yBAAyB,EAC/B,EAAE,EAAE,yBAAyB,GAC5B,OAAO,CAAC,eAAe,CAAC,CAG1B"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export async function exportTomorrowOSData(store) {
|
|
2
|
+
const [pendingCodes, deviceRegistry, pairedDevices, playlists] = await Promise.all([
|
|
3
|
+
store.listPendingCodes(),
|
|
4
|
+
store.listDeviceRegistry(),
|
|
5
|
+
store.listPairedDevices(),
|
|
6
|
+
store.listPlaylists()
|
|
7
|
+
]);
|
|
8
|
+
const deviceAssignments = await Promise.all(pairedDevices.map(async ({ deviceId }) => ({
|
|
9
|
+
deviceId,
|
|
10
|
+
assignments: await store.getDeviceAssignments(deviceId)
|
|
11
|
+
})));
|
|
12
|
+
return {
|
|
13
|
+
pendingCodes,
|
|
14
|
+
deviceRegistry,
|
|
15
|
+
pairedDevices,
|
|
16
|
+
playlists,
|
|
17
|
+
deviceAssignments
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export async function importTomorrowOSData(store, snapshot) {
|
|
21
|
+
for (const { deviceId, record } of snapshot.deviceRegistry) {
|
|
22
|
+
await store.setDeviceRegistry(deviceId, record);
|
|
23
|
+
}
|
|
24
|
+
for (const { deviceId, record } of snapshot.pairedDevices) {
|
|
25
|
+
await store.setPairedDevice(deviceId, record);
|
|
26
|
+
}
|
|
27
|
+
for (const playlist of snapshot.playlists) {
|
|
28
|
+
await store.setPlaylist(playlist);
|
|
29
|
+
}
|
|
30
|
+
for (const { deviceId, assignments } of snapshot.deviceAssignments) {
|
|
31
|
+
await store.setDeviceAssignments(deviceId, assignments);
|
|
32
|
+
}
|
|
33
|
+
for (const { code, record } of snapshot.pendingCodes) {
|
|
34
|
+
await store.setPendingCode(code, record);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
pendingCodes: snapshot.pendingCodes.length,
|
|
38
|
+
deviceRegistry: snapshot.deviceRegistry.length,
|
|
39
|
+
pairedDevices: snapshot.pairedDevices.length,
|
|
40
|
+
playlists: snapshot.playlists.length,
|
|
41
|
+
deviceAssignments: snapshot.deviceAssignments.reduce((total, entry) => total + entry.assignments.length, 0)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export async function migrateTomorrowOSData(from, to) {
|
|
45
|
+
const snapshot = await exportTomorrowOSData(from);
|
|
46
|
+
return importTomorrowOSData(to, snapshot);
|
|
47
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type PoolConfig } from "pg";
|
|
2
|
+
import type { DevicePlaylistAssignment, DeviceRegistryEntry, DeviceRegistryRecord, PairedDeviceEntry, PairedDeviceRecord, PendingCodeEntry, PendingCodeRecord, StoredPlaylist, TomorrowOSMigratableStore } from "./types.js";
|
|
3
|
+
export interface PostgresStoreOptions {
|
|
4
|
+
connectionString: string;
|
|
5
|
+
ssl?: PoolConfig["ssl"];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Durable store backed by Postgres. Supabase works through its Postgres
|
|
9
|
+
* connection string, usually provided as DATABASE_URL.
|
|
10
|
+
*/
|
|
11
|
+
export declare class PostgresStore implements TomorrowOSMigratableStore {
|
|
12
|
+
private readonly pool;
|
|
13
|
+
private ready?;
|
|
14
|
+
constructor(options: PostgresStoreOptions | string);
|
|
15
|
+
init(): Promise<void>;
|
|
16
|
+
close(): Promise<void>;
|
|
17
|
+
private ensureReady;
|
|
18
|
+
setPendingCode(code: string, record: PendingCodeRecord): Promise<void>;
|
|
19
|
+
getPendingCode(code: string): Promise<PendingCodeRecord | undefined>;
|
|
20
|
+
deletePendingCode(code: string): Promise<void>;
|
|
21
|
+
listPendingCodes(): Promise<PendingCodeEntry[]>;
|
|
22
|
+
getDeviceRegistry(deviceId: string): Promise<DeviceRegistryRecord | undefined>;
|
|
23
|
+
setDeviceRegistry(deviceId: string, record: DeviceRegistryRecord): Promise<void>;
|
|
24
|
+
getDeviceRegistryByCode(code: string): Promise<{
|
|
25
|
+
deviceId: string;
|
|
26
|
+
record: DeviceRegistryRecord;
|
|
27
|
+
} | undefined>;
|
|
28
|
+
listDeviceRegistry(): Promise<DeviceRegistryEntry[]>;
|
|
29
|
+
setPairedDevice(deviceId: string, record: PairedDeviceRecord): Promise<void>;
|
|
30
|
+
getPairedDevice(deviceId: string): Promise<PairedDeviceRecord | undefined>;
|
|
31
|
+
deletePairedDevice(deviceId: string): Promise<void>;
|
|
32
|
+
listPairedDevices(): Promise<PairedDeviceEntry[]>;
|
|
33
|
+
listPlaylists(): Promise<StoredPlaylist[]>;
|
|
34
|
+
getPlaylist(id: string): Promise<StoredPlaylist | undefined>;
|
|
35
|
+
setPlaylist(record: StoredPlaylist): Promise<void>;
|
|
36
|
+
isPlaylistNameTaken(name: string, excludeId?: string): Promise<boolean>;
|
|
37
|
+
getDeviceAssignments(deviceId: string): Promise<DevicePlaylistAssignment[]>;
|
|
38
|
+
setDeviceAssignments(deviceId: string, assignments: DevicePlaylistAssignment[]): Promise<void>;
|
|
39
|
+
private mapDeviceRegistryRow;
|
|
40
|
+
private mapPairedDeviceRow;
|
|
41
|
+
private mapPlaylistRow;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=postgres-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-store.d.ts","sourceRoot":"","sources":["../../src/store/postgres-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,IAAI,CAAC;AAE3C,OAAO,KAAK,EACV,wBAAwB,EACxB,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EAGjB,cAAc,EACd,yBAAyB,EAC1B,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,oBAAoB;IACnC,gBAAgB,EAAE,MAAM,CAAC;IACzB,GAAG,CAAC,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC;CACzB;AAyDD;;;GAGG;AACH,qBAAa,aAAc,YAAW,yBAAyB;IAC7D,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAO;IAC5B,OAAO,CAAC,KAAK,CAAC,CAAgB;gBAElB,OAAO,EAAE,oBAAoB,GAAG,MAAM;IAY5C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAsErB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAId,WAAW;IAKnB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAWtE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAepE,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAK9C,gBAAgB,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAiB/C,iBAAiB,CACrB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC;IAWtC,iBAAiB,CACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,IAAI,CAAC;IA4BV,uBAAuB,CAC3B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,oBAAoB,CAAA;KAAE,GAAG,SAAS,CAAC;IAepE,kBAAkB,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAapD,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,IAAI,CAAC;IAwCV,eAAe,CACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC;IAWpC,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOnD,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAajD,aAAa,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAU1C,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAW5D,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAkClD,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAevE,oBAAoB,CACxB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,wBAAwB,EAAE,CAAC;IAqBhC,oBAAoB,CACxB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,wBAAwB,EAAE,GACtC,OAAO,CAAC,IAAI,CAAC;IAuChB,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,kBAAkB;IAc1B,OAAO,CAAC,cAAc;CAYvB"}
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { Pool } from "pg";
|
|
2
|
+
function optionalString(value) {
|
|
3
|
+
return value ?? undefined;
|
|
4
|
+
}
|
|
5
|
+
function optionalNumber(value) {
|
|
6
|
+
if (value == null)
|
|
7
|
+
return undefined;
|
|
8
|
+
const parsed = Number(value);
|
|
9
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
10
|
+
}
|
|
11
|
+
function parseJson(raw, fallback) {
|
|
12
|
+
if (!raw)
|
|
13
|
+
return fallback;
|
|
14
|
+
return JSON.parse(raw);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Durable store backed by Postgres. Supabase works through its Postgres
|
|
18
|
+
* connection string, usually provided as DATABASE_URL.
|
|
19
|
+
*/
|
|
20
|
+
export class PostgresStore {
|
|
21
|
+
pool;
|
|
22
|
+
ready;
|
|
23
|
+
constructor(options) {
|
|
24
|
+
const connectionString = typeof options === "string" ? options : options.connectionString;
|
|
25
|
+
const ssl = typeof options === "string" ? undefined : options.ssl;
|
|
26
|
+
if (!connectionString.trim()) {
|
|
27
|
+
throw new Error("PostgresStore requires a connection string.");
|
|
28
|
+
}
|
|
29
|
+
this.pool = new Pool({ connectionString, ssl });
|
|
30
|
+
}
|
|
31
|
+
async init() {
|
|
32
|
+
await this.pool.query(`
|
|
33
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
34
|
+
id INTEGER PRIMARY KEY,
|
|
35
|
+
name TEXT NOT NULL UNIQUE,
|
|
36
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
CREATE TABLE IF NOT EXISTS pending_codes (
|
|
40
|
+
code TEXT PRIMARY KEY,
|
|
41
|
+
device_id TEXT NOT NULL,
|
|
42
|
+
created_at BIGINT NOT NULL
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
CREATE TABLE IF NOT EXISTS device_registry (
|
|
46
|
+
device_id TEXT PRIMARY KEY,
|
|
47
|
+
permanent_pairing_code TEXT NOT NULL UNIQUE,
|
|
48
|
+
code_created_at BIGINT NOT NULL,
|
|
49
|
+
serial_number TEXT,
|
|
50
|
+
first_seen_at BIGINT,
|
|
51
|
+
last_hello_at BIGINT
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
CREATE TABLE IF NOT EXISTS paired_devices (
|
|
55
|
+
device_id TEXT PRIMARY KEY,
|
|
56
|
+
pairing_token TEXT NOT NULL,
|
|
57
|
+
paired_at TEXT NOT NULL,
|
|
58
|
+
device_name TEXT,
|
|
59
|
+
platform TEXT,
|
|
60
|
+
system TEXT,
|
|
61
|
+
last_boot_at TEXT,
|
|
62
|
+
last_online_at TEXT,
|
|
63
|
+
last_offline_at TEXT,
|
|
64
|
+
last_policy_push_at TEXT
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
CREATE TABLE IF NOT EXISTS playlists (
|
|
68
|
+
id TEXT PRIMARY KEY,
|
|
69
|
+
name TEXT NOT NULL,
|
|
70
|
+
schedule_json TEXT,
|
|
71
|
+
items_json TEXT NOT NULL DEFAULT '[]',
|
|
72
|
+
version INTEGER NOT NULL,
|
|
73
|
+
updated_at TEXT NOT NULL,
|
|
74
|
+
retired BOOLEAN NOT NULL DEFAULT false,
|
|
75
|
+
retired_at TEXT
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
CREATE UNIQUE INDEX IF NOT EXISTS playlists_active_name_unique
|
|
79
|
+
ON playlists (lower(trim(name)))
|
|
80
|
+
WHERE retired = false;
|
|
81
|
+
|
|
82
|
+
CREATE TABLE IF NOT EXISTS device_assignments (
|
|
83
|
+
device_id TEXT NOT NULL,
|
|
84
|
+
playlist_id TEXT NOT NULL,
|
|
85
|
+
sort_order INTEGER NOT NULL,
|
|
86
|
+
published_version INTEGER NOT NULL,
|
|
87
|
+
published_at TEXT NOT NULL,
|
|
88
|
+
snapshot_json TEXT NOT NULL,
|
|
89
|
+
PRIMARY KEY (device_id, playlist_id)
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
CREATE INDEX IF NOT EXISTS device_assignments_device_order_idx
|
|
93
|
+
ON device_assignments (device_id, sort_order);
|
|
94
|
+
|
|
95
|
+
INSERT INTO schema_migrations (id, name)
|
|
96
|
+
VALUES (1, 'initial_tomorrowos_store')
|
|
97
|
+
ON CONFLICT (id) DO NOTHING;
|
|
98
|
+
`);
|
|
99
|
+
}
|
|
100
|
+
async close() {
|
|
101
|
+
await this.pool.end();
|
|
102
|
+
}
|
|
103
|
+
async ensureReady() {
|
|
104
|
+
this.ready ??= this.init();
|
|
105
|
+
await this.ready;
|
|
106
|
+
}
|
|
107
|
+
async setPendingCode(code, record) {
|
|
108
|
+
await this.ensureReady();
|
|
109
|
+
await this.pool.query(`
|
|
110
|
+
INSERT INTO pending_codes (code, device_id, created_at)
|
|
111
|
+
VALUES ($1, $2, $3)
|
|
112
|
+
ON CONFLICT (code) DO UPDATE SET
|
|
113
|
+
device_id = EXCLUDED.device_id,
|
|
114
|
+
created_at = EXCLUDED.created_at
|
|
115
|
+
`, [code, record.deviceId, record.createdAt]);
|
|
116
|
+
}
|
|
117
|
+
async getPendingCode(code) {
|
|
118
|
+
await this.ensureReady();
|
|
119
|
+
const result = await this.pool.query(`
|
|
120
|
+
SELECT device_id, created_at
|
|
121
|
+
FROM pending_codes
|
|
122
|
+
WHERE code = $1
|
|
123
|
+
`, [code]);
|
|
124
|
+
const row = result.rows[0];
|
|
125
|
+
if (!row)
|
|
126
|
+
return undefined;
|
|
127
|
+
return { deviceId: row.device_id, createdAt: Number(row.created_at) };
|
|
128
|
+
}
|
|
129
|
+
async deletePendingCode(code) {
|
|
130
|
+
await this.ensureReady();
|
|
131
|
+
await this.pool.query("DELETE FROM pending_codes WHERE code = $1", [code]);
|
|
132
|
+
}
|
|
133
|
+
async listPendingCodes() {
|
|
134
|
+
await this.ensureReady();
|
|
135
|
+
const result = await this.pool.query(`
|
|
136
|
+
SELECT code, device_id, created_at
|
|
137
|
+
FROM pending_codes
|
|
138
|
+
ORDER BY created_at ASC
|
|
139
|
+
`);
|
|
140
|
+
return result.rows.map((row) => ({
|
|
141
|
+
code: row.code,
|
|
142
|
+
record: { deviceId: row.device_id, createdAt: Number(row.created_at) }
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
async getDeviceRegistry(deviceId) {
|
|
146
|
+
await this.ensureReady();
|
|
147
|
+
const result = await this.pool.query(`
|
|
148
|
+
SELECT *
|
|
149
|
+
FROM device_registry
|
|
150
|
+
WHERE device_id = $1
|
|
151
|
+
`, [deviceId]);
|
|
152
|
+
const row = result.rows[0];
|
|
153
|
+
return row ? this.mapDeviceRegistryRow(row) : undefined;
|
|
154
|
+
}
|
|
155
|
+
async setDeviceRegistry(deviceId, record) {
|
|
156
|
+
await this.ensureReady();
|
|
157
|
+
await this.pool.query(`
|
|
158
|
+
INSERT INTO device_registry (
|
|
159
|
+
device_id,
|
|
160
|
+
permanent_pairing_code,
|
|
161
|
+
code_created_at,
|
|
162
|
+
serial_number,
|
|
163
|
+
first_seen_at,
|
|
164
|
+
last_hello_at
|
|
165
|
+
)
|
|
166
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
167
|
+
ON CONFLICT (device_id) DO UPDATE SET
|
|
168
|
+
permanent_pairing_code = EXCLUDED.permanent_pairing_code,
|
|
169
|
+
code_created_at = EXCLUDED.code_created_at,
|
|
170
|
+
serial_number = EXCLUDED.serial_number,
|
|
171
|
+
first_seen_at = EXCLUDED.first_seen_at,
|
|
172
|
+
last_hello_at = EXCLUDED.last_hello_at
|
|
173
|
+
`, [
|
|
174
|
+
deviceId,
|
|
175
|
+
record.permanentPairingCode,
|
|
176
|
+
record.codeCreatedAt,
|
|
177
|
+
record.serialNumber ?? null,
|
|
178
|
+
record.firstSeenAt ?? null,
|
|
179
|
+
record.lastHelloAt ?? null
|
|
180
|
+
]);
|
|
181
|
+
}
|
|
182
|
+
async getDeviceRegistryByCode(code) {
|
|
183
|
+
await this.ensureReady();
|
|
184
|
+
const result = await this.pool.query(`
|
|
185
|
+
SELECT *
|
|
186
|
+
FROM device_registry
|
|
187
|
+
WHERE permanent_pairing_code = $1
|
|
188
|
+
`, [code]);
|
|
189
|
+
const row = result.rows[0];
|
|
190
|
+
if (!row)
|
|
191
|
+
return undefined;
|
|
192
|
+
return {
|
|
193
|
+
deviceId: row.device_id,
|
|
194
|
+
record: this.mapDeviceRegistryRow(row)
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
async listDeviceRegistry() {
|
|
198
|
+
await this.ensureReady();
|
|
199
|
+
const result = await this.pool.query(`
|
|
200
|
+
SELECT *
|
|
201
|
+
FROM device_registry
|
|
202
|
+
ORDER BY code_created_at ASC
|
|
203
|
+
`);
|
|
204
|
+
return result.rows.map((row) => ({
|
|
205
|
+
deviceId: row.device_id,
|
|
206
|
+
record: this.mapDeviceRegistryRow(row)
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
209
|
+
async setPairedDevice(deviceId, record) {
|
|
210
|
+
await this.ensureReady();
|
|
211
|
+
await this.pool.query(`
|
|
212
|
+
INSERT INTO paired_devices (
|
|
213
|
+
device_id,
|
|
214
|
+
pairing_token,
|
|
215
|
+
paired_at,
|
|
216
|
+
device_name,
|
|
217
|
+
platform,
|
|
218
|
+
system,
|
|
219
|
+
last_boot_at,
|
|
220
|
+
last_online_at,
|
|
221
|
+
last_offline_at,
|
|
222
|
+
last_policy_push_at
|
|
223
|
+
)
|
|
224
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
225
|
+
ON CONFLICT (device_id) DO UPDATE SET
|
|
226
|
+
pairing_token = EXCLUDED.pairing_token,
|
|
227
|
+
paired_at = EXCLUDED.paired_at,
|
|
228
|
+
device_name = EXCLUDED.device_name,
|
|
229
|
+
platform = EXCLUDED.platform,
|
|
230
|
+
system = EXCLUDED.system,
|
|
231
|
+
last_boot_at = EXCLUDED.last_boot_at,
|
|
232
|
+
last_online_at = EXCLUDED.last_online_at,
|
|
233
|
+
last_offline_at = EXCLUDED.last_offline_at,
|
|
234
|
+
last_policy_push_at = EXCLUDED.last_policy_push_at
|
|
235
|
+
`, [
|
|
236
|
+
deviceId,
|
|
237
|
+
record.pairingToken,
|
|
238
|
+
record.pairedAt,
|
|
239
|
+
record.deviceName ?? null,
|
|
240
|
+
record.platform ?? null,
|
|
241
|
+
record.system ?? null,
|
|
242
|
+
record.lastBootAt ?? null,
|
|
243
|
+
record.lastOnlineAt ?? null,
|
|
244
|
+
record.lastOfflineAt ?? null,
|
|
245
|
+
record.lastPolicyPushAt ?? null
|
|
246
|
+
]);
|
|
247
|
+
}
|
|
248
|
+
async getPairedDevice(deviceId) {
|
|
249
|
+
await this.ensureReady();
|
|
250
|
+
const result = await this.pool.query(`
|
|
251
|
+
SELECT *
|
|
252
|
+
FROM paired_devices
|
|
253
|
+
WHERE device_id = $1
|
|
254
|
+
`, [deviceId]);
|
|
255
|
+
const row = result.rows[0];
|
|
256
|
+
return row ? this.mapPairedDeviceRow(row) : undefined;
|
|
257
|
+
}
|
|
258
|
+
async deletePairedDevice(deviceId) {
|
|
259
|
+
await this.ensureReady();
|
|
260
|
+
await this.pool.query("DELETE FROM paired_devices WHERE device_id = $1", [
|
|
261
|
+
deviceId
|
|
262
|
+
]);
|
|
263
|
+
}
|
|
264
|
+
async listPairedDevices() {
|
|
265
|
+
await this.ensureReady();
|
|
266
|
+
const result = await this.pool.query(`
|
|
267
|
+
SELECT *
|
|
268
|
+
FROM paired_devices
|
|
269
|
+
ORDER BY paired_at ASC
|
|
270
|
+
`);
|
|
271
|
+
return result.rows.map((row) => ({
|
|
272
|
+
deviceId: row.device_id,
|
|
273
|
+
record: this.mapPairedDeviceRow(row)
|
|
274
|
+
}));
|
|
275
|
+
}
|
|
276
|
+
async listPlaylists() {
|
|
277
|
+
await this.ensureReady();
|
|
278
|
+
const result = await this.pool.query(`
|
|
279
|
+
SELECT *
|
|
280
|
+
FROM playlists
|
|
281
|
+
ORDER BY updated_at DESC
|
|
282
|
+
`);
|
|
283
|
+
return result.rows.map((row) => this.mapPlaylistRow(row));
|
|
284
|
+
}
|
|
285
|
+
async getPlaylist(id) {
|
|
286
|
+
await this.ensureReady();
|
|
287
|
+
const result = await this.pool.query(`
|
|
288
|
+
SELECT *
|
|
289
|
+
FROM playlists
|
|
290
|
+
WHERE id = $1
|
|
291
|
+
`, [id]);
|
|
292
|
+
const row = result.rows[0];
|
|
293
|
+
return row ? this.mapPlaylistRow(row) : undefined;
|
|
294
|
+
}
|
|
295
|
+
async setPlaylist(record) {
|
|
296
|
+
await this.ensureReady();
|
|
297
|
+
await this.pool.query(`
|
|
298
|
+
INSERT INTO playlists (
|
|
299
|
+
id,
|
|
300
|
+
name,
|
|
301
|
+
schedule_json,
|
|
302
|
+
items_json,
|
|
303
|
+
version,
|
|
304
|
+
updated_at,
|
|
305
|
+
retired,
|
|
306
|
+
retired_at
|
|
307
|
+
)
|
|
308
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
309
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
310
|
+
name = EXCLUDED.name,
|
|
311
|
+
schedule_json = EXCLUDED.schedule_json,
|
|
312
|
+
items_json = EXCLUDED.items_json,
|
|
313
|
+
version = EXCLUDED.version,
|
|
314
|
+
updated_at = EXCLUDED.updated_at,
|
|
315
|
+
retired = EXCLUDED.retired,
|
|
316
|
+
retired_at = EXCLUDED.retired_at
|
|
317
|
+
`, [
|
|
318
|
+
record.id,
|
|
319
|
+
record.name,
|
|
320
|
+
record.schedule ? JSON.stringify(record.schedule) : null,
|
|
321
|
+
JSON.stringify(record.items ?? []),
|
|
322
|
+
record.version,
|
|
323
|
+
record.updatedAt,
|
|
324
|
+
record.retired === true,
|
|
325
|
+
record.retiredAt ?? null
|
|
326
|
+
]);
|
|
327
|
+
}
|
|
328
|
+
async isPlaylistNameTaken(name, excludeId) {
|
|
329
|
+
await this.ensureReady();
|
|
330
|
+
const target = name.trim().toLowerCase();
|
|
331
|
+
if (!target)
|
|
332
|
+
return false;
|
|
333
|
+
const result = await this.pool.query(`
|
|
334
|
+
SELECT id
|
|
335
|
+
FROM playlists
|
|
336
|
+
WHERE retired = false
|
|
337
|
+
AND lower(trim(name)) = $1
|
|
338
|
+
AND ($2::text IS NULL OR id != $2)
|
|
339
|
+
LIMIT 1
|
|
340
|
+
`, [target, excludeId ?? null]);
|
|
341
|
+
return result.rows.length > 0;
|
|
342
|
+
}
|
|
343
|
+
async getDeviceAssignments(deviceId) {
|
|
344
|
+
await this.ensureReady();
|
|
345
|
+
const result = await this.pool.query(`
|
|
346
|
+
SELECT playlist_id, published_version, published_at, snapshot_json
|
|
347
|
+
FROM device_assignments
|
|
348
|
+
WHERE device_id = $1
|
|
349
|
+
ORDER BY sort_order ASC
|
|
350
|
+
`, [deviceId]);
|
|
351
|
+
return result.rows.map((row) => ({
|
|
352
|
+
playlistId: row.playlist_id,
|
|
353
|
+
publishedVersion: row.published_version,
|
|
354
|
+
publishedAt: row.published_at,
|
|
355
|
+
snapshot: parseJson(row.snapshot_json, {
|
|
356
|
+
id: row.playlist_id,
|
|
357
|
+
name: row.playlist_id,
|
|
358
|
+
version: row.published_version,
|
|
359
|
+
items: []
|
|
360
|
+
})
|
|
361
|
+
}));
|
|
362
|
+
}
|
|
363
|
+
async setDeviceAssignments(deviceId, assignments) {
|
|
364
|
+
await this.ensureReady();
|
|
365
|
+
const client = await this.pool.connect();
|
|
366
|
+
try {
|
|
367
|
+
await client.query("BEGIN");
|
|
368
|
+
await client.query("DELETE FROM device_assignments WHERE device_id = $1", [
|
|
369
|
+
deviceId
|
|
370
|
+
]);
|
|
371
|
+
for (const [index, assignment] of assignments.entries()) {
|
|
372
|
+
await client.query(`
|
|
373
|
+
INSERT INTO device_assignments (
|
|
374
|
+
device_id,
|
|
375
|
+
playlist_id,
|
|
376
|
+
sort_order,
|
|
377
|
+
published_version,
|
|
378
|
+
published_at,
|
|
379
|
+
snapshot_json
|
|
380
|
+
)
|
|
381
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
382
|
+
`, [
|
|
383
|
+
deviceId,
|
|
384
|
+
assignment.playlistId,
|
|
385
|
+
index,
|
|
386
|
+
assignment.publishedVersion,
|
|
387
|
+
assignment.publishedAt,
|
|
388
|
+
JSON.stringify(assignment.snapshot)
|
|
389
|
+
]);
|
|
390
|
+
}
|
|
391
|
+
await client.query("COMMIT");
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
await client.query("ROLLBACK");
|
|
395
|
+
throw err;
|
|
396
|
+
}
|
|
397
|
+
finally {
|
|
398
|
+
client.release();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
mapDeviceRegistryRow(row) {
|
|
402
|
+
return {
|
|
403
|
+
permanentPairingCode: row.permanent_pairing_code,
|
|
404
|
+
codeCreatedAt: Number(row.code_created_at),
|
|
405
|
+
serialNumber: optionalString(row.serial_number),
|
|
406
|
+
firstSeenAt: optionalNumber(row.first_seen_at),
|
|
407
|
+
lastHelloAt: optionalNumber(row.last_hello_at)
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
mapPairedDeviceRow(row) {
|
|
411
|
+
return {
|
|
412
|
+
pairingToken: row.pairing_token,
|
|
413
|
+
pairedAt: row.paired_at,
|
|
414
|
+
deviceName: optionalString(row.device_name),
|
|
415
|
+
platform: optionalString(row.platform),
|
|
416
|
+
system: optionalString(row.system),
|
|
417
|
+
lastBootAt: optionalString(row.last_boot_at),
|
|
418
|
+
lastOnlineAt: optionalString(row.last_online_at),
|
|
419
|
+
lastOfflineAt: optionalString(row.last_offline_at),
|
|
420
|
+
lastPolicyPushAt: optionalString(row.last_policy_push_at)
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
mapPlaylistRow(row) {
|
|
424
|
+
return {
|
|
425
|
+
id: row.id,
|
|
426
|
+
name: row.name,
|
|
427
|
+
schedule: parseJson(row.schedule_json, undefined),
|
|
428
|
+
items: parseJson(row.items_json, []),
|
|
429
|
+
version: row.version,
|
|
430
|
+
updatedAt: row.updated_at,
|
|
431
|
+
retired: row.retired,
|
|
432
|
+
retiredAt: optionalString(row.retired_at)
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DevicePlaylistAssignment, DeviceRegistryRecord, PairedDeviceEntry, PairedDeviceRecord, PendingCodeRecord, StoredPlaylist,
|
|
1
|
+
import type { DevicePlaylistAssignment, DeviceRegistryEntry, DeviceRegistryRecord, PairedDeviceEntry, PairedDeviceRecord, PendingCodeEntry, PendingCodeRecord, StoredPlaylist, TomorrowOSMigratableStore } from "./types.js";
|
|
2
2
|
interface SQLiteStoreOptions {
|
|
3
3
|
databasePath?: string;
|
|
4
4
|
}
|
|
@@ -6,7 +6,7 @@ interface SQLiteStoreOptions {
|
|
|
6
6
|
* Durable single-node store backed by a local SQLite database.
|
|
7
7
|
* For multi-instance production, inject a Postgres/Supabase-backed TomorrowOSStore instead.
|
|
8
8
|
*/
|
|
9
|
-
export declare class SQLiteStore implements
|
|
9
|
+
export declare class SQLiteStore implements TomorrowOSMigratableStore {
|
|
10
10
|
readonly databasePath: string;
|
|
11
11
|
private readonly db;
|
|
12
12
|
constructor(options?: SQLiteStoreOptions | string);
|
|
@@ -15,12 +15,14 @@ export declare class SQLiteStore implements TomorrowOSStore {
|
|
|
15
15
|
setPendingCode(code: string, record: PendingCodeRecord): Promise<void>;
|
|
16
16
|
getPendingCode(code: string): Promise<PendingCodeRecord | undefined>;
|
|
17
17
|
deletePendingCode(code: string): Promise<void>;
|
|
18
|
+
listPendingCodes(): Promise<PendingCodeEntry[]>;
|
|
18
19
|
getDeviceRegistry(deviceId: string): Promise<DeviceRegistryRecord | undefined>;
|
|
19
20
|
setDeviceRegistry(deviceId: string, record: DeviceRegistryRecord): Promise<void>;
|
|
20
21
|
getDeviceRegistryByCode(code: string): Promise<{
|
|
21
22
|
deviceId: string;
|
|
22
23
|
record: DeviceRegistryRecord;
|
|
23
24
|
} | undefined>;
|
|
25
|
+
listDeviceRegistry(): Promise<DeviceRegistryEntry[]>;
|
|
24
26
|
setPairedDevice(deviceId: string, record: PairedDeviceRecord): Promise<void>;
|
|
25
27
|
getPairedDevice(deviceId: string): Promise<PairedDeviceRecord | undefined>;
|
|
26
28
|
deletePairedDevice(deviceId: string): Promise<void>;
|
|
@@ -1 +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,
|
|
1
|
+
{"version":3,"file":"sqlite-store.d.ts","sourceRoot":"","sources":["../../src/store/sqlite-store.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,wBAAwB,EACxB,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EAGjB,cAAc,EACd,yBAAyB,EAC1B,MAAM,YAAY,CAAC;AAEpB,UAAU,kBAAkB;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA+DD;;;GAGG;AACH,qBAAa,WAAY,YAAW,yBAAyB;IAC3D,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,gBAAgB,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAY/C,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,kBAAkB,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAYpD,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"}
|
|
@@ -127,6 +127,17 @@ export class SQLiteStore {
|
|
|
127
127
|
async deletePendingCode(code) {
|
|
128
128
|
this.db.prepare("DELETE FROM pending_codes WHERE code = ?").run(code);
|
|
129
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
|
+
}
|
|
130
141
|
async getDeviceRegistry(deviceId) {
|
|
131
142
|
const row = this.db.prepare(`
|
|
132
143
|
SELECT *
|
|
@@ -167,6 +178,17 @@ export class SQLiteStore {
|
|
|
167
178
|
record: this.mapDeviceRegistryRow(row)
|
|
168
179
|
};
|
|
169
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
|
+
}
|
|
170
192
|
async setPairedDevice(deviceId, record) {
|
|
171
193
|
this.db.prepare(`
|
|
172
194
|
INSERT INTO paired_devices (
|
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",
|
|
@@ -33,11 +33,13 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"better-sqlite3": "^12.11.1",
|
|
36
|
+
"pg": "^8.22.0",
|
|
36
37
|
"ws": "^8.18.0"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@types/better-sqlite3": "^7.6.13",
|
|
40
41
|
"@types/node": "^20.19.41",
|
|
42
|
+
"@types/pg": "^8.20.0",
|
|
41
43
|
"@types/ws": "^8.5.10",
|
|
42
44
|
"typescript": "^5.5.0"
|
|
43
45
|
},
|
|
@@ -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",
|
|
@@ -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
|
+
}
|