@astrojs/db 0.2.1 → 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.
- package/config-augment.d.ts +1 -1
- package/dist/core/cli/commands/link/index.d.ts +8 -0
- package/dist/core/cli/commands/link/index.js +64 -0
- package/dist/core/cli/commands/login/index.d.ts +6 -0
- package/dist/core/cli/commands/login/index.js +46 -0
- package/dist/core/cli/commands/logout/index.d.ts +6 -0
- package/dist/core/cli/commands/logout/index.js +9 -0
- package/dist/core/cli/commands/push/index.js +71 -43
- package/dist/core/cli/commands/shell/index.js +5 -8
- package/dist/core/cli/commands/sync/index.js +22 -29
- package/dist/core/cli/commands/verify/index.d.ts +1 -1
- package/dist/core/cli/commands/verify/index.js +20 -16
- package/dist/core/cli/index.js +29 -8
- package/dist/core/cli/migration-queries.d.ts +19 -11
- package/dist/core/cli/migration-queries.js +124 -161
- package/dist/core/cli/migrations.d.ts +22 -1
- package/dist/core/cli/migrations.js +66 -2
- package/dist/core/errors.d.ts +5 -1
- package/dist/core/errors.js +35 -5
- package/dist/core/integration/index.js +32 -17
- package/dist/core/integration/typegen.js +2 -2
- package/dist/core/integration/vite-plugin-db.d.ts +1 -1
- package/dist/core/integration/vite-plugin-db.js +6 -4
- package/dist/core/queries.d.ts +57 -1
- package/dist/core/queries.js +70 -23
- package/dist/core/tokens.d.ts +11 -0
- package/dist/core/tokens.js +131 -0
- package/dist/core/types.d.ts +7049 -1903
- package/dist/core/types.js +133 -60
- package/dist/core/utils.d.ts +1 -0
- package/dist/core/utils.js +5 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +8 -2
- package/dist/runtime/db-client.js +1 -1
- package/dist/runtime/index.d.ts +5 -1
- package/dist/runtime/index.js +36 -21
- package/dist/runtime/types.d.ts +13 -3
- package/dist/runtime/types.js +8 -0
- package/package.json +9 -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 {
|
|
10
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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.
|
|
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 {
|
|
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({
|
|
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
|
|
29
|
+
import dbUrl from ${JSON.stringify(`${dbUrl}?fileurl`)};
|
|
28
30
|
|
|
29
31
|
const params = ${JSON.stringify({
|
|
30
32
|
collections,
|
package/dist/core/queries.d.ts
CHANGED
|
@@ -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
|
-
|
|
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 {};
|
package/dist/core/queries.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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 (
|
|
37
|
+
} catch (error) {
|
|
43
38
|
(logger ?? console).error(
|
|
44
|
-
`Failed to seed data. Did you update to match recent schema changes
|
|
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 =
|
|
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.
|
|
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
|
+
};
|