@astrojs/db 0.2.2 → 0.3.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.
Files changed (39) hide show
  1. package/config-augment.d.ts +1 -1
  2. package/dist/core/cli/commands/link/index.d.ts +8 -0
  3. package/dist/core/cli/commands/link/index.js +64 -0
  4. package/dist/core/cli/commands/login/index.d.ts +6 -0
  5. package/dist/core/cli/commands/login/index.js +46 -0
  6. package/dist/core/cli/commands/logout/index.d.ts +6 -0
  7. package/dist/core/cli/commands/logout/index.js +9 -0
  8. package/dist/core/cli/commands/push/index.js +71 -43
  9. package/dist/core/cli/commands/shell/index.js +5 -8
  10. package/dist/core/cli/commands/sync/index.js +22 -29
  11. package/dist/core/cli/commands/verify/index.d.ts +1 -1
  12. package/dist/core/cli/commands/verify/index.js +20 -16
  13. package/dist/core/cli/index.js +29 -8
  14. package/dist/core/cli/migration-queries.d.ts +19 -11
  15. package/dist/core/cli/migration-queries.js +124 -161
  16. package/dist/core/cli/migrations.d.ts +22 -1
  17. package/dist/core/cli/migrations.js +66 -2
  18. package/dist/core/errors.d.ts +5 -1
  19. package/dist/core/errors.js +35 -5
  20. package/dist/core/integration/index.js +32 -17
  21. package/dist/core/integration/typegen.js +2 -2
  22. package/dist/core/integration/vite-plugin-db.d.ts +1 -1
  23. package/dist/core/integration/vite-plugin-db.js +6 -4
  24. package/dist/core/queries.d.ts +57 -1
  25. package/dist/core/queries.js +70 -23
  26. package/dist/core/tokens.d.ts +11 -0
  27. package/dist/core/tokens.js +131 -0
  28. package/dist/core/types.d.ts +7049 -1903
  29. package/dist/core/types.js +133 -60
  30. package/dist/core/utils.d.ts +1 -0
  31. package/dist/core/utils.js +5 -0
  32. package/dist/index.d.ts +2 -1
  33. package/dist/index.js +8 -2
  34. package/dist/runtime/db-client.js +1 -1
  35. package/dist/runtime/index.d.ts +5 -1
  36. package/dist/runtime/index.js +36 -21
  37. package/dist/runtime/types.d.ts +13 -3
  38. package/dist/runtime/types.js +8 -0
  39. package/package.json +5 -3
@@ -6,42 +6,42 @@ import { mkdir, rm, writeFile } from "fs/promises";
6
6
  import { DB_PATH } from "../consts.js";
7
7
  import { createLocalDatabaseClient } from "../../runtime/db-client.js";
8
8
  import { astroConfigWithDbSchema } from "../types.js";
9
- import { getAstroStudioEnv } from "../utils.js";
10
- import { appTokenError } from "../errors.js";
9
+ import {} from "../utils.js";
10
+ import { STUDIO_CONFIG_MISSING_WRITABLE_COLLECTIONS_ERROR } from "../errors.js";
11
11
  import { errorMap } from "./error-map.js";
12
12
  import { dirname } from "path";
13
13
  import { fileURLToPath } from "url";
14
- import { bold } from "kleur/colors";
14
+ import { blue, yellow } from "kleur/colors";
15
15
  import { fileURLIntegration } from "./file-url.js";
16
16
  import { setupDbTables } from "../queries.js";
17
+ import { getManagedAppTokenOrExit } from "../tokens.js";
17
18
  function astroDBIntegration() {
19
+ let connectedToRemote = false;
20
+ let appToken;
18
21
  return {
19
22
  name: "astro:db",
20
23
  hooks: {
21
- async "astro:config:setup"({ logger, updateConfig, config, command }) {
24
+ "astro:config:setup": async ({ logger, updateConfig, config, command }) => {
22
25
  if (command === "preview")
23
26
  return;
24
27
  const configWithDb = astroConfigWithDbSchema.parse(config, { errorMap });
25
28
  const collections = configWithDb.db?.collections ?? {};
26
29
  const studio = configWithDb.db?.studio ?? false;
27
- if (!studio && Object.values(collections).some((c) => c.writable)) {
28
- logger.warn(
29
- `Writable collections should only be used with Astro Studio. Did you set the ${bold(
30
- "studio"
31
- )} flag in your astro config?`
30
+ const foundWritableCollection = Object.entries(collections).find(([, c]) => c.writable);
31
+ if (!studio && foundWritableCollection) {
32
+ logger.error(
33
+ STUDIO_CONFIG_MISSING_WRITABLE_COLLECTIONS_ERROR(foundWritableCollection[0])
32
34
  );
35
+ process.exit(1);
33
36
  }
34
37
  let dbPlugin;
35
- if (studio && command === "build") {
36
- const appToken = getAstroStudioEnv().ASTRO_STUDIO_APP_TOKEN;
37
- if (!appToken) {
38
- logger.error(appTokenError);
39
- process.exit(0);
40
- }
38
+ if (studio && command === "build" && process.env.ASTRO_DB_TEST_ENV !== "1") {
39
+ appToken = await getManagedAppTokenOrExit();
40
+ connectedToRemote = true;
41
41
  dbPlugin = vitePluginDb({
42
42
  connectToStudio: true,
43
43
  collections,
44
- appToken,
44
+ appToken: appToken.token,
45
45
  root: config.root
46
46
  });
47
47
  } else {
@@ -63,7 +63,7 @@ function astroDBIntegration() {
63
63
  logger,
64
64
  mode: command === "dev" ? "dev" : "build"
65
65
  });
66
- logger.info("Collections set up \u{1F680}");
66
+ logger.debug("Database setup complete.");
67
67
  dbPlugin = vitePluginDb({
68
68
  connectToStudio: false,
69
69
  collections,
@@ -91,6 +91,21 @@ function astroDBIntegration() {
91
91
  }
92
92
  });
93
93
  await typegen({ collections, root: config.root });
94
+ },
95
+ "astro:server:start": async ({ logger }) => {
96
+ setTimeout(() => {
97
+ logger.info(
98
+ connectedToRemote ? "Connected to remote database." : "New local database created."
99
+ );
100
+ }, 100);
101
+ },
102
+ "astro:build:start": async ({ logger }) => {
103
+ logger.info(
104
+ "database: " + (connectedToRemote ? yellow("remote") : blue("local database."))
105
+ );
106
+ },
107
+ "astro:build:done": async ({}) => {
108
+ await appToken?.destroy();
94
109
  }
95
110
  }
96
111
  };
@@ -27,8 +27,8 @@ function generateTableType(name, collection) {
27
27
  {
28
28
  // Only select fields Drizzle needs for inference
29
29
  type: field.type,
30
- optional: field.optional,
31
- default: field.default
30
+ optional: field.schema.optional,
31
+ default: field.schema.default
32
32
  }
33
33
  ])
34
34
  )
@@ -10,7 +10,7 @@ export declare function vitePluginDb(params: {
10
10
  appToken: string;
11
11
  root: URL;
12
12
  }): VitePlugin;
13
- export declare function getVirtualModContents({ collections, root }: {
13
+ export declare function getVirtualModContents({ collections, root, }: {
14
14
  collections: DBCollections;
15
15
  root: URL;
16
16
  }): string;
@@ -1,5 +1,4 @@
1
- import { RUNTIME_IMPORT, VIRTUAL_MODULE_ID, DB_PATH, RUNTIME_DRIZZLE_IMPORT } from "../consts.js";
2
- import { fileURLToPath } from "node:url";
1
+ import { DB_PATH, RUNTIME_DRIZZLE_IMPORT, RUNTIME_IMPORT, VIRTUAL_MODULE_ID } from "../consts.js";
3
2
  const resolvedVirtualModuleId = "\0" + VIRTUAL_MODULE_ID;
4
3
  function vitePluginDb(params) {
5
4
  return {
@@ -20,11 +19,14 @@ function vitePluginDb(params) {
20
19
  }
21
20
  };
22
21
  }
23
- function getVirtualModContents({ collections, root }) {
22
+ function getVirtualModContents({
23
+ collections,
24
+ root
25
+ }) {
24
26
  const dbUrl = new URL(DB_PATH, root);
25
27
  return `
26
28
  import { collectionToTable, createLocalDatabaseClient } from ${RUNTIME_IMPORT};
27
- import dbUrl from '${fileURLToPath(dbUrl)}?fileurl';
29
+ import dbUrl from ${JSON.stringify(`${dbUrl}?fileurl`)};
28
30
 
29
31
  const params = ${JSON.stringify({
30
32
  collections,
@@ -11,9 +11,65 @@ export declare function setupDbTables({ db, data, collections, logger, mode, }:
11
11
  }): Promise<void>;
12
12
  export declare function getCreateTableQuery(collectionName: string, collection: DBCollection): string;
13
13
  export declare function getCreateIndexQueries(collectionName: string, collection: Pick<DBCollection, 'indexes'>): string[];
14
+ export declare function getCreateForeignKeyQueries(collectionName: string, collection: DBCollection): string[];
14
15
  export declare function schemaTypeToSqlType(type: FieldType): 'text' | 'integer';
15
16
  export declare function getModifiers(fieldName: string, field: DBField): string;
16
- type WithDefaultDefined<T extends DBField> = T & Required<Pick<T, 'default'>>;
17
+ export declare function getReferencesConfig(field: DBField): {
18
+ type: "number";
19
+ schema: ({
20
+ name?: string | undefined;
21
+ label?: string | undefined;
22
+ unique?: boolean | undefined;
23
+ collection?: string | undefined;
24
+ } & {
25
+ primaryKey?: false | undefined;
26
+ optional?: boolean | undefined;
27
+ default?: number | import("../runtime/types.js").SerializedSQL | undefined;
28
+ } & {
29
+ references?: any | undefined;
30
+ }) | ({
31
+ name?: string | undefined;
32
+ label?: string | undefined;
33
+ unique?: boolean | undefined;
34
+ collection?: string | undefined;
35
+ } & {
36
+ primaryKey: true;
37
+ optional?: false | undefined;
38
+ default?: undefined;
39
+ } & {
40
+ references?: any | undefined;
41
+ });
42
+ } | {
43
+ type: "text";
44
+ schema: ({
45
+ name?: string | undefined;
46
+ label?: string | undefined;
47
+ unique?: boolean | undefined;
48
+ collection?: string | undefined;
49
+ default?: string | import("../runtime/types.js").SerializedSQL | undefined;
50
+ multiline?: boolean | undefined;
51
+ } & {
52
+ primaryKey?: false | undefined;
53
+ optional?: boolean | undefined;
54
+ } & {
55
+ references?: any | undefined;
56
+ }) | ({
57
+ name?: string | undefined;
58
+ label?: string | undefined;
59
+ unique?: boolean | undefined;
60
+ collection?: string | undefined;
61
+ default?: string | import("../runtime/types.js").SerializedSQL | undefined;
62
+ multiline?: boolean | undefined;
63
+ } & {
64
+ primaryKey: true;
65
+ optional?: false | undefined;
66
+ } & {
67
+ references?: any | undefined;
68
+ });
69
+ } | undefined;
70
+ type WithDefaultDefined<T extends DBField> = T & {
71
+ schema: Required<Pick<T['schema'], 'default'>>;
72
+ };
17
73
  type DBFieldWithDefault = WithDefaultDefined<TextField> | WithDefaultDefined<DateField> | WithDefaultDefined<NumberField> | WithDefaultDefined<BooleanField> | WithDefaultDefined<JsonField>;
18
74
  export declare function hasDefault(field: DBField): field is DBFieldWithDefault;
19
75
  export {};
@@ -3,7 +3,8 @@ import {
3
3
  import { bold } from "kleur/colors";
4
4
  import { sql } from "drizzle-orm";
5
5
  import { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core";
6
- import { collectionToTable, hasPrimaryKey } from "../runtime/index.js";
6
+ import { hasPrimaryKey } from "../runtime/index.js";
7
+ import { isSerializedSQL } from "../runtime/types.js";
7
8
  const sqlite = new SQLiteAsyncDialect();
8
9
  async function setupDbTables({
9
10
  db,
@@ -11,6 +12,7 @@ async function setupDbTables({
11
12
  collections,
12
13
  logger,
13
14
  mode
15
+ // TODO: Remove once Turso has foreign key PRAGMA support
14
16
  }) {
15
17
  const setupQueries = [];
16
18
  for (const [name, collection] of Object.entries(collections)) {
@@ -23,28 +25,20 @@ async function setupDbTables({
23
25
  await db.run(q);
24
26
  }
25
27
  if (data) {
26
- for (const [name, collection] of Object.entries(collections)) {
27
- const table = collectionToTable(name, collection);
28
- collection._setMeta?.({ table });
29
- }
30
28
  try {
31
29
  await data({
32
- async seed({ table }, values) {
33
- const result = Array.isArray(values) ? (
34
- // TODO: fix values typing once we can infer fields type correctly
35
- await db.insert(table).values(values).returning()
36
- ) : await db.insert(table).values(values).returning().get();
30
+ seed: async ({ table }, values) => {
31
+ const result = Array.isArray(values) ? db.insert(table).values(values).returning() : db.insert(table).values(values).returning().get();
37
32
  return result;
38
33
  },
39
34
  db,
40
35
  mode
41
36
  });
42
- } catch (e) {
37
+ } catch (error) {
43
38
  (logger ?? console).error(
44
- `Failed to seed data. Did you update to match recent schema changes? Full error:
45
-
46
- ${e}`
39
+ `Failed to seed data. Did you update to match recent schema changes?`
47
40
  );
41
+ (logger ?? console).error(error);
48
42
  }
49
43
  }
50
44
  }
@@ -63,13 +57,14 @@ function getCreateTableQuery(collectionName, collection) {
63
57
  )}${getModifiers(columnName, column)}`;
64
58
  colQueries.push(colQuery);
65
59
  }
60
+ colQueries.push(...getCreateForeignKeyQueries(collectionName, collection));
66
61
  query += colQueries.join(", ") + ")";
67
62
  return query;
68
63
  }
69
64
  function getCreateIndexQueries(collectionName, collection) {
70
65
  let queries = [];
71
66
  for (const [indexName, indexProps] of Object.entries(collection.indexes ?? {})) {
72
- const onColNames = Array.isArray(indexProps.on) ? indexProps.on : [indexProps.on];
67
+ const onColNames = asArray(indexProps.on);
73
68
  const onCols = onColNames.map((colName) => sqlite.escapeName(colName));
74
69
  const unique = indexProps.unique ? "UNIQUE " : "";
75
70
  const indexQuery = `CREATE ${unique}INDEX ${sqlite.escapeName(
@@ -79,6 +74,30 @@ function getCreateIndexQueries(collectionName, collection) {
79
74
  }
80
75
  return queries;
81
76
  }
77
+ function getCreateForeignKeyQueries(collectionName, collection) {
78
+ let queries = [];
79
+ for (const foreignKey of collection.foreignKeys ?? []) {
80
+ const fields = asArray(foreignKey.fields);
81
+ const references = asArray(foreignKey.references);
82
+ if (fields.length !== references.length) {
83
+ throw new Error(
84
+ `Foreign key on ${collectionName} is misconfigured. \`fields\` and \`references\` must be the same length.`
85
+ );
86
+ }
87
+ const referencedCollection = references[0]?.schema.collection;
88
+ if (!referencedCollection) {
89
+ throw new Error(
90
+ `Foreign key on ${collectionName} is misconfigured. \`references\` cannot be empty.`
91
+ );
92
+ }
93
+ const query = `FOREIGN KEY (${fields.map((f) => sqlite.escapeName(f)).join(", ")}) REFERENCES ${sqlite.escapeName(referencedCollection)}(${references.map((r) => sqlite.escapeName(r.schema.name)).join(", ")})`;
94
+ queries.push(query);
95
+ }
96
+ return queries;
97
+ }
98
+ function asArray(value) {
99
+ return Array.isArray(value) ? value : [value];
100
+ }
82
101
  function schemaTypeToSqlType(type) {
83
102
  switch (type) {
84
103
  case "date":
@@ -95,19 +114,35 @@ function getModifiers(fieldName, field) {
95
114
  if (hasPrimaryKey(field)) {
96
115
  return " PRIMARY KEY";
97
116
  }
98
- if (!field.optional) {
117
+ if (!field.schema.optional) {
99
118
  modifiers += " NOT NULL";
100
119
  }
101
- if (field.unique) {
120
+ if (field.schema.unique) {
102
121
  modifiers += " UNIQUE";
103
122
  }
104
123
  if (hasDefault(field)) {
105
124
  modifiers += ` DEFAULT ${getDefaultValueSql(fieldName, field)}`;
106
125
  }
126
+ const references = getReferencesConfig(field);
127
+ if (references) {
128
+ const { collection, name } = references.schema;
129
+ if (!collection || !name) {
130
+ throw new Error(
131
+ `Invalid reference for field ${fieldName}. This is an unexpected error that should be reported to the Astro team.`
132
+ );
133
+ }
134
+ modifiers += ` REFERENCES ${sqlite.escapeName(collection)} (${sqlite.escapeName(name)})`;
135
+ }
107
136
  return modifiers;
108
137
  }
138
+ function getReferencesConfig(field) {
139
+ const canHaveReferences = field.type === "number" || field.type === "text";
140
+ if (!canHaveReferences)
141
+ return void 0;
142
+ return field.schema.references;
143
+ }
109
144
  function hasDefault(field) {
110
- if (field.default !== void 0) {
145
+ if (field.schema.default !== void 0) {
111
146
  return true;
112
147
  }
113
148
  if (hasPrimaryKey(field) && field.type === "number") {
@@ -115,20 +150,30 @@ function hasDefault(field) {
115
150
  }
116
151
  return false;
117
152
  }
153
+ function toDefault(def) {
154
+ const type = typeof def;
155
+ if (type === "string") {
156
+ return sqlite.escapeString(def);
157
+ } else if (type === "boolean") {
158
+ return def ? "TRUE" : "FALSE";
159
+ } else {
160
+ return def + "";
161
+ }
162
+ }
118
163
  function getDefaultValueSql(columnName, column) {
164
+ if (isSerializedSQL(column.schema.default)) {
165
+ return column.schema.default.sql;
166
+ }
119
167
  switch (column.type) {
120
168
  case "boolean":
121
- return column.default ? "TRUE" : "FALSE";
122
169
  case "number":
123
- return `${column.default || "AUTOINCREMENT"}`;
124
170
  case "text":
125
- return sqlite.escapeString(column.default);
126
171
  case "date":
127
- return column.default === "now" ? "CURRENT_TIMESTAMP" : sqlite.escapeString(column.default);
172
+ return toDefault(column.schema.default);
128
173
  case "json": {
129
174
  let stringified = "";
130
175
  try {
131
- stringified = JSON.stringify(column.default);
176
+ stringified = JSON.stringify(column.schema.default);
132
177
  } catch (e) {
133
178
  console.log(
134
179
  `Invalid default value for column ${bold(
@@ -142,9 +187,11 @@ function getDefaultValueSql(columnName, column) {
142
187
  }
143
188
  }
144
189
  export {
190
+ getCreateForeignKeyQueries,
145
191
  getCreateIndexQueries,
146
192
  getCreateTableQuery,
147
193
  getModifiers,
194
+ getReferencesConfig,
148
195
  hasDefault,
149
196
  schemaTypeToSqlType,
150
197
  setupDbTables
@@ -0,0 +1,11 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ export declare const SESSION_LOGIN_FILE: import("url").URL;
3
+ export declare const PROJECT_ID_FILE: import("url").URL;
4
+ export interface ManagedAppToken {
5
+ token: string;
6
+ renew(): Promise<void>;
7
+ destroy(): Promise<void>;
8
+ }
9
+ export declare function getProjectIdFromFile(): Promise<string | undefined>;
10
+ export declare function getSessionIdFromFile(): Promise<string | undefined>;
11
+ export declare function getManagedAppTokenOrExit(token?: string): Promise<ManagedAppToken>;
@@ -0,0 +1,131 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { getAstroStudioEnv, getAstroStudioUrl } from "./utils.js";
6
+ import { MISSING_PROJECT_ID_ERROR, MISSING_SESSION_ID_ERROR } from "./errors.js";
7
+ const SESSION_LOGIN_FILE = pathToFileURL(join(homedir(), ".astro", "session-token"));
8
+ const PROJECT_ID_FILE = pathToFileURL(join(process.cwd(), ".astro", "link"));
9
+ class ManagedLocalAppToken {
10
+ token;
11
+ constructor(token) {
12
+ this.token = token;
13
+ }
14
+ async renew() {
15
+ }
16
+ async destroy() {
17
+ }
18
+ }
19
+ class ManagedRemoteAppToken {
20
+ token;
21
+ session;
22
+ projectId;
23
+ ttl;
24
+ renewTimer;
25
+ static async create(sessionToken, projectId) {
26
+ const response = await fetch(new URL(`${getAstroStudioUrl()}/auth/cli/token-create`), {
27
+ method: "POST",
28
+ headers: new Headers({
29
+ Authorization: `Bearer ${sessionToken}`
30
+ }),
31
+ body: JSON.stringify({ projectId })
32
+ });
33
+ const { token: shortLivedAppToken, ttl } = await response.json();
34
+ return new ManagedRemoteAppToken({
35
+ token: shortLivedAppToken,
36
+ session: sessionToken,
37
+ projectId,
38
+ ttl
39
+ });
40
+ }
41
+ constructor(options) {
42
+ this.token = options.token;
43
+ this.session = options.session;
44
+ this.projectId = options.projectId;
45
+ this.ttl = options.ttl;
46
+ this.renewTimer = setTimeout(() => this.renew(), 1e3 * 60 * 5 / 2);
47
+ }
48
+ async fetch(url, body) {
49
+ return fetch(`${getAstroStudioUrl()}${url}`, {
50
+ method: "POST",
51
+ headers: {
52
+ Authorization: `Bearer ${this.session}`,
53
+ "Content-Type": "application/json"
54
+ },
55
+ body: JSON.stringify(body)
56
+ });
57
+ }
58
+ async renew() {
59
+ clearTimeout(this.renewTimer);
60
+ delete this.renewTimer;
61
+ try {
62
+ const response = await this.fetch("/auth/cli/token-renew", {
63
+ token: this.token,
64
+ projectId: this.projectId
65
+ });
66
+ if (response.status === 200) {
67
+ this.renewTimer = setTimeout(() => this.renew(), 1e3 * 60 * this.ttl / 2);
68
+ } else {
69
+ throw new Error(`Unexpected response: ${response.status} ${response.statusText}`);
70
+ }
71
+ } catch (error) {
72
+ const retryIn = 60 * this.ttl / 10;
73
+ console.error(`Failed to renew token. Retrying in ${retryIn} seconds.`, error?.message);
74
+ this.renewTimer = setTimeout(() => this.renew(), retryIn * 1e3);
75
+ }
76
+ }
77
+ async destroy() {
78
+ try {
79
+ const response = await this.fetch("/auth/cli/token-delete", {
80
+ token: this.token,
81
+ projectId: this.projectId
82
+ });
83
+ if (response.status !== 200) {
84
+ throw new Error(`Unexpected response: ${response.status} ${response.statusText}`);
85
+ }
86
+ } catch (error) {
87
+ console.error("Failed to delete token.", error?.message);
88
+ }
89
+ }
90
+ }
91
+ async function getProjectIdFromFile() {
92
+ try {
93
+ return await readFile(PROJECT_ID_FILE, "utf-8");
94
+ } catch (error) {
95
+ return void 0;
96
+ }
97
+ }
98
+ async function getSessionIdFromFile() {
99
+ try {
100
+ return await readFile(SESSION_LOGIN_FILE, "utf-8");
101
+ } catch (error) {
102
+ return void 0;
103
+ }
104
+ }
105
+ async function getManagedAppTokenOrExit(token) {
106
+ if (token) {
107
+ return new ManagedLocalAppToken(token);
108
+ }
109
+ const { ASTRO_STUDIO_APP_TOKEN } = getAstroStudioEnv();
110
+ if (ASTRO_STUDIO_APP_TOKEN) {
111
+ return new ManagedLocalAppToken(ASTRO_STUDIO_APP_TOKEN);
112
+ }
113
+ const sessionToken = await getSessionIdFromFile();
114
+ if (!sessionToken) {
115
+ console.error(MISSING_SESSION_ID_ERROR);
116
+ process.exit(1);
117
+ }
118
+ const projectId = await getProjectIdFromFile();
119
+ if (!sessionToken || !projectId) {
120
+ console.error(MISSING_PROJECT_ID_ERROR);
121
+ process.exit(1);
122
+ }
123
+ return ManagedRemoteAppToken.create(sessionToken, projectId);
124
+ }
125
+ export {
126
+ PROJECT_ID_FILE,
127
+ SESSION_LOGIN_FILE,
128
+ getManagedAppTokenOrExit,
129
+ getProjectIdFromFile,
130
+ getSessionIdFromFile
131
+ };