@tomorrowos/sdk 0.4.8 → 0.5.1

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
@@ -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
- 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.
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
+ includes its own `README.md` beside `server.ts` with database selection,
55
+ migration, and custom `TomorrowOSStore` examples.
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 main() {
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
@@ -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";
@@ -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 sqlite.
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":"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"}
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"}
@@ -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 !== "sqlite") {
15
- throw new Error(`Unsupported TOMORROWOS_STORE "${driver}". Use "sqlite", "memory", or pass a custom TomorrowOSStore to new TomorrowOS({ store }).`);
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 (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.");
26
+ if (driver === "sqlite") {
27
+ return new SQLiteStore({
28
+ databasePath: options.sqlitePath ?? env.TOMORROWOS_DB_PATH ?? defaultSQLitePath()
29
+ });
19
30
  }
20
- return new SQLiteStore({
21
- databasePath: options.sqlitePath ?? env.TOMORROWOS_DB_PATH ?? defaultSQLitePath()
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, TomorrowOSStore } from "./types.js";
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 TomorrowOSStore {
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,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"}
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 (
@@ -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.4.8",
3
+ "version": "0.5.1",
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
@@ -0,0 +1,192 @@
1
+ # TomorrowOS CMS Starter
2
+
3
+ This starter runs a TomorrowOS CMS server with a small built-in control panel.
4
+ The entry point is `server.ts`.
5
+
6
+ ## Start
7
+
8
+ ```bash
9
+ npm install
10
+ npm run dev
11
+ ```
12
+
13
+ By default, the starter uses SQLite at `data/tomorrowos.db`. The database is
14
+ created by `tomorrowos init` and the server will also create missing tables on
15
+ startup.
16
+
17
+ ## Choose A Database
18
+
19
+ Use `.env` to select the database.
20
+
21
+ Default local SQLite:
22
+
23
+ ```env
24
+ TOMORROWOS_STORE=sqlite
25
+ TOMORROWOS_DB_PATH=./data/tomorrowos.db
26
+ ```
27
+
28
+ Supabase/Postgres:
29
+
30
+ ```env
31
+ TOMORROWOS_STORE=supabase
32
+ DATABASE_URL=postgresql://postgres:[PASSWORD]@[HOST]:5432/postgres
33
+ DATABASE_SSL=true
34
+ ```
35
+
36
+ Throwaway in-memory store for tests only:
37
+
38
+ ```env
39
+ TOMORROWOS_STORE=memory
40
+ ```
41
+
42
+ The default `server.ts` uses `createTomorrowOSStore()`, so changing `.env` is
43
+ enough when you use the SDK's built-in SQLite or Supabase/Postgres schema.
44
+
45
+ ## Migrate Data
46
+
47
+ Move data from SQLite to Supabase:
48
+
49
+ ```bash
50
+ npx tomorrowos migrate \
51
+ --from sqlite \
52
+ --from-sqlite ./data/tomorrowos.db \
53
+ --to supabase \
54
+ --to-database-url "$DATABASE_URL"
55
+ ```
56
+
57
+ Move data from Supabase back to SQLite:
58
+
59
+ ```bash
60
+ npx tomorrowos migrate \
61
+ --from supabase \
62
+ --from-database-url "$DATABASE_URL" \
63
+ --to sqlite \
64
+ --to-sqlite ./data/tomorrowos.db
65
+ ```
66
+
67
+ The migration command copies TomorrowOS database records: pairing codes, device
68
+ registry records, paired devices, playlists, and device assignments. It does not
69
+ copy media files in `public/uploads` or object storage. Copy those separately.
70
+
71
+ ## Create Your Own Store
72
+
73
+ Use a custom store when you want your own table names, column names, or database
74
+ backend. Implement `TomorrowOSStore`; implement `TomorrowOSMigratableStore` too
75
+ if you want your custom store to participate in migrations.
76
+
77
+ Reference the SDK's built-in implementations for production-grade examples:
78
+
79
+ - `@tomorrowos/sdk/dist/store/sqlite-store.js`
80
+ - `@tomorrowos/sdk/dist/store/postgres-store.js`
81
+
82
+ In TypeScript projects or when working from the SDK source, the matching source
83
+ files are `src/store/sqlite-store.ts` and `src/store/postgres-store.ts`.
84
+
85
+ Example skeleton:
86
+
87
+ ```ts
88
+ import type {
89
+ DevicePlaylistAssignment,
90
+ DeviceRegistryEntry,
91
+ DeviceRegistryRecord,
92
+ PairedDeviceEntry,
93
+ PairedDeviceRecord,
94
+ PendingCodeEntry,
95
+ PendingCodeRecord,
96
+ StoredPlaylist,
97
+ TomorrowOSMigratableStore
98
+ } from "@tomorrowos/sdk";
99
+
100
+ export class MyDatabaseStore implements TomorrowOSMigratableStore {
101
+ constructor(private readonly connectionString: string) {}
102
+
103
+ async setPendingCode(code: string, record: PendingCodeRecord): Promise<void> {
104
+ // Write record.deviceId and record.createdAt into your schema.
105
+ }
106
+
107
+ async getPendingCode(code: string): Promise<PendingCodeRecord | undefined> {
108
+ // Read from your schema and map it back to { deviceId, createdAt }.
109
+ return undefined;
110
+ }
111
+
112
+ async deletePendingCode(code: string): Promise<void> {}
113
+
114
+ async listPendingCodes(): Promise<PendingCodeEntry[]> {
115
+ return [];
116
+ }
117
+
118
+ async getDeviceRegistry(deviceId: string): Promise<DeviceRegistryRecord | undefined> {
119
+ return undefined;
120
+ }
121
+
122
+ async setDeviceRegistry(deviceId: string, record: DeviceRegistryRecord): Promise<void> {}
123
+
124
+ async getDeviceRegistryByCode(
125
+ code: string
126
+ ): Promise<{ deviceId: string; record: DeviceRegistryRecord } | undefined> {
127
+ return undefined;
128
+ }
129
+
130
+ async listDeviceRegistry(): Promise<DeviceRegistryEntry[]> {
131
+ return [];
132
+ }
133
+
134
+ async setPairedDevice(deviceId: string, record: PairedDeviceRecord): Promise<void> {}
135
+
136
+ async getPairedDevice(deviceId: string): Promise<PairedDeviceRecord | undefined> {
137
+ return undefined;
138
+ }
139
+
140
+ async deletePairedDevice(deviceId: string): Promise<void> {}
141
+
142
+ async listPairedDevices(): Promise<PairedDeviceEntry[]> {
143
+ return [];
144
+ }
145
+
146
+ async listPlaylists(): Promise<StoredPlaylist[]> {
147
+ return [];
148
+ }
149
+
150
+ async getPlaylist(id: string): Promise<StoredPlaylist | undefined> {
151
+ return undefined;
152
+ }
153
+
154
+ async setPlaylist(record: StoredPlaylist): Promise<void> {}
155
+
156
+ async isPlaylistNameTaken(name: string, excludeId?: string): Promise<boolean> {
157
+ return false;
158
+ }
159
+
160
+ async getDeviceAssignments(deviceId: string): Promise<DevicePlaylistAssignment[]> {
161
+ return [];
162
+ }
163
+
164
+ async setDeviceAssignments(
165
+ deviceId: string,
166
+ assignments: DevicePlaylistAssignment[]
167
+ ): Promise<void> {}
168
+ }
169
+ ```
170
+
171
+ ## Use Your Store In server.ts
172
+
173
+ Replace the default store creation:
174
+
175
+ ```ts
176
+ const store = createTomorrowOSStore({
177
+ sqlitePath: join(__dirname, "data", "tomorrowos.db")
178
+ });
179
+ ```
180
+
181
+ with your custom store:
182
+
183
+ ```ts
184
+ import { TomorrowOS } from "@tomorrowos/sdk";
185
+ import { MyDatabaseStore } from "./stores/my-database-store.js";
186
+
187
+ const store = new MyDatabaseStore(process.env.DATABASE_URL!);
188
+ const tomorrowos = new TomorrowOS({ brand, store });
189
+ ```
190
+
191
+ Your store class name and file path can be anything. TomorrowOS only requires
192
+ that the object passed as `store` implements the required store methods.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "my-cms",
3
- "version": "0.4.8",
3
+ "version": "0.5.1",
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.4.8"
13
+ "@tomorrowos/sdk": "^0.5.1",
14
+ "dotenv": "^17.2.3"
14
15
  },
15
16
  "devDependencies": {
16
17
  "@types/node": "^20.0.0",
@@ -3,6 +3,7 @@
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";