@stoker-platform/cli 0.5.82 → 0.5.86
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/lib/package.json +4 -4
- package/lib/src/deploy/deployProject.js +1 -0
- package/lib/src/deploy/schema/generateSchema.js +186 -154
- package/lib/src/lint/lintSchema.js +8 -11
- package/lib/src/main.js +8 -0
- package/lib/src/types/generateTypes.js +107 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
package/lib/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stoker-platform/cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.85",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"main": "./lib/src/main.js",
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"@google-cloud/secret-manager": "^6.1.1",
|
|
25
25
|
"@google-cloud/storage": "^7.19.0",
|
|
26
26
|
"@inquirer/prompts": "^8.4.2",
|
|
27
|
-
"@stoker-platform/node-client": "0.5.
|
|
28
|
-
"@stoker-platform/types": "0.5.
|
|
29
|
-
"@stoker-platform/utils": "0.5.
|
|
27
|
+
"@stoker-platform/node-client": "0.5.59",
|
|
28
|
+
"@stoker-platform/types": "0.5.38",
|
|
29
|
+
"@stoker-platform/utils": "0.5.50",
|
|
30
30
|
"algoliasearch": "^5.51.0",
|
|
31
31
|
"commander": "^14.0.0",
|
|
32
32
|
"cross-spawn": "^7.0.6",
|
|
@@ -25,6 +25,7 @@ export const deployProject = async (options) => {
|
|
|
25
25
|
throw new Error("The schema for this project has changed. Maintenance mode cannot be disabled.");
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
await runChildProcess("npx", ["stoker", "generate-types"]);
|
|
28
29
|
await runChildProcess("npm", ["run", "format"]);
|
|
29
30
|
await runChildProcess("npm", ["run", "lint"]);
|
|
30
31
|
await runChildProcess("npm", ["run", "build"]);
|
|
@@ -1,184 +1,216 @@
|
|
|
1
1
|
import { tryPromise } from "@stoker-platform/node-client";
|
|
2
2
|
import { getRelationLists, roleHasOperationAccess } from "@stoker-platform/utils";
|
|
3
3
|
import { ServerValue } from "firebase-admin/database";
|
|
4
|
-
import {
|
|
4
|
+
import { spawnSync } from "child_process";
|
|
5
|
+
import { existsSync } from "fs";
|
|
6
|
+
import { readdir, readFile, rm } from "fs/promises";
|
|
5
7
|
import { join } from "path";
|
|
6
8
|
import { pathToFileURL } from "url";
|
|
7
|
-
|
|
9
|
+
const ensureSchemaBuildOutput = (isTypeGen) => {
|
|
10
|
+
if (!isTypeGen)
|
|
11
|
+
return;
|
|
12
|
+
const mainPath = join(process.cwd(), "lib", "main.js");
|
|
13
|
+
const collectionsPath = join(process.cwd(), "lib", "collections");
|
|
14
|
+
if (existsSync(mainPath) && existsSync(collectionsPath))
|
|
15
|
+
return;
|
|
16
|
+
const buildResult = spawnSync("npx", ["tsc", "--pretty", "false", "--noEmitOnError", "false"], {
|
|
17
|
+
cwd: process.cwd(),
|
|
18
|
+
stdio: "ignore",
|
|
19
|
+
});
|
|
20
|
+
if (buildResult.error) {
|
|
21
|
+
throw new Error("Failed to run TypeScript compile step for schema generation", { cause: buildResult.error });
|
|
22
|
+
}
|
|
23
|
+
if (!existsSync(mainPath) || !existsSync(collectionsPath)) {
|
|
24
|
+
throw new Error("No lib output found. Run 'npm run build' to generate lib artifacts.");
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
};
|
|
28
|
+
export const generateSchema = async (includeComputedFields = false, isTypeGen = false) => {
|
|
8
29
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
30
|
const newSchema = {};
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const globalConfig = globalConfigFile.default({ sdk: "node" });
|
|
14
|
-
const projectData = await readFile(join(process.cwd(), "project-data.json"), "utf8");
|
|
15
|
-
const projectDataJson = JSON.parse(projectData);
|
|
16
|
-
const version = projectDataJson.version || 1;
|
|
17
|
-
newSchema.version = version;
|
|
18
|
-
newSchema.published_time = ServerValue.TIMESTAMP;
|
|
19
|
-
const collections = await readdir(join(process.cwd(), "lib", "collections"));
|
|
20
|
-
const { roles, firebase } = globalConfig;
|
|
21
|
-
const configSchema = {
|
|
22
|
-
roles,
|
|
23
|
-
permissionsIndexExemption: false,
|
|
24
|
-
};
|
|
25
|
-
if (firebase?.permissionsIndexExemption)
|
|
26
|
-
configSchema.permissionsIndexExemption = true;
|
|
27
|
-
firebase?.writeLogIndexExemption
|
|
28
|
-
? (configSchema.writeLogIndexExemption = firebase?.writeLogIndexExemption || [])
|
|
29
|
-
: null;
|
|
30
|
-
firebase?.writeLogTTL ? (configSchema.writeLogTTL = firebase?.writeLogTTL) : null;
|
|
31
|
-
newSchema.config = configSchema;
|
|
32
|
-
const fullSchema = [];
|
|
33
|
-
for (const collection of collections) {
|
|
34
|
-
if (globalConfig.disabledCollections?.includes(collection))
|
|
35
|
-
continue;
|
|
36
|
-
const path = join(process.cwd(), "lib", "collections", collection);
|
|
31
|
+
const tempSchemaGenerated = ensureSchemaBuildOutput(isTypeGen);
|
|
32
|
+
try {
|
|
33
|
+
const path = join(process.cwd(), "lib", "main.js");
|
|
37
34
|
const url = pathToFileURL(path).href;
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
|
|
35
|
+
const globalConfigFile = await import(url);
|
|
36
|
+
const globalConfig = globalConfigFile.default({ sdk: "node" });
|
|
37
|
+
const projectData = await readFile(join(process.cwd(), "project-data.json"), "utf8");
|
|
38
|
+
const projectDataJson = JSON.parse(projectData);
|
|
39
|
+
const version = projectDataJson.version || 1;
|
|
40
|
+
newSchema.version = version;
|
|
41
|
+
newSchema.published_time = ServerValue.TIMESTAMP;
|
|
42
|
+
const collections = await readdir(join(process.cwd(), "lib", "collections"));
|
|
43
|
+
const { roles, firebase } = globalConfig;
|
|
44
|
+
const configSchema = {
|
|
45
|
+
roles,
|
|
46
|
+
permissionsIndexExemption: false,
|
|
47
|
+
};
|
|
48
|
+
if (firebase?.permissionsIndexExemption)
|
|
49
|
+
configSchema.permissionsIndexExemption = true;
|
|
50
|
+
firebase?.writeLogIndexExemption
|
|
51
|
+
? (configSchema.writeLogIndexExemption = firebase?.writeLogIndexExemption || [])
|
|
52
|
+
: null;
|
|
53
|
+
firebase?.writeLogTTL ? (configSchema.writeLogTTL = firebase?.writeLogTTL) : null;
|
|
54
|
+
newSchema.config = configSchema;
|
|
55
|
+
const fullSchema = [];
|
|
56
|
+
for (const collection of collections) {
|
|
57
|
+
if (globalConfig.disabledCollections?.includes(collection))
|
|
58
|
+
continue;
|
|
59
|
+
const path = join(process.cwd(), "lib", "collections", collection);
|
|
60
|
+
const url = pathToFileURL(path).href;
|
|
61
|
+
const schema = await import(url);
|
|
62
|
+
fullSchema.push(schema.default({ sdk: "node" }));
|
|
53
63
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
newSchema.collections = {};
|
|
65
|
+
for (const collection of collections) {
|
|
66
|
+
if (globalConfig.disabledCollections?.includes(collection))
|
|
67
|
+
continue;
|
|
68
|
+
const path = join(process.cwd(), "lib", "collections", collection);
|
|
69
|
+
const url = pathToFileURL(path).href;
|
|
70
|
+
const schema = await import(url);
|
|
71
|
+
const persistSchema = schema.default({ sdk: "node" });
|
|
72
|
+
const { labels, access, preloadCache, admin } = persistSchema;
|
|
73
|
+
const { serverReadOnly } = access;
|
|
74
|
+
if (!includeComputedFields) {
|
|
75
|
+
persistSchema.fields = persistSchema.fields.filter((field) => field.type !== "Computed");
|
|
60
76
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const nonStandardRoles = (serverReadOnly || []).concat(preloadCache?.roles || []);
|
|
69
|
-
const standardRoles = readRoles.filter((role) => !nonStandardRoles.includes(role));
|
|
70
|
-
const noPreloadCacheRoles = readRoles.filter((role) => !preloadCache?.roles.includes(role));
|
|
71
|
-
const addUnscheduledRoles = calendar?.unscheduled?.roles && !noPreloadCacheRoles.length;
|
|
72
|
-
if (calendar?.unscheduled) {
|
|
73
|
-
filters.push({
|
|
74
|
-
type: "select",
|
|
75
|
-
field: calendar.startField,
|
|
76
|
-
roles: calendar?.unscheduled.roles,
|
|
77
|
-
});
|
|
77
|
+
else {
|
|
78
|
+
for (const field of persistSchema.fields) {
|
|
79
|
+
if (field.type === "Computed") {
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
81
|
+
delete field.formula;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
78
84
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
if (admin) {
|
|
86
|
+
const filters = ((await tryPromise(admin.filters)) || []);
|
|
87
|
+
const cards = (await tryPromise(admin.cards));
|
|
88
|
+
const calendar = (await tryPromise(admin.calendar));
|
|
89
|
+
const statusField = (await tryPromise(admin.statusField));
|
|
90
|
+
const readRoles = roles.filter((role) => roleHasOperationAccess(persistSchema, role, "read"));
|
|
91
|
+
const nonStandardRoles = (serverReadOnly || []).concat(preloadCache?.roles || []);
|
|
92
|
+
const standardRoles = readRoles.filter((role) => !nonStandardRoles.includes(role));
|
|
93
|
+
const noPreloadCacheRoles = readRoles.filter((role) => !preloadCache?.roles.includes(role));
|
|
94
|
+
const addUnscheduledRoles = calendar?.unscheduled?.roles && !noPreloadCacheRoles.length;
|
|
95
|
+
if (calendar?.unscheduled) {
|
|
96
|
+
filters.push({
|
|
97
|
+
type: "select",
|
|
98
|
+
field: calendar.startField,
|
|
99
|
+
roles: calendar?.unscheduled.roles,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const relationListFields = getRelationLists(labels.collection, fullSchema);
|
|
103
|
+
if (relationListFields.size > 0) {
|
|
104
|
+
for (const relationListField of relationListFields.values()) {
|
|
105
|
+
const existingRelationFilter = filters.find((filter) => filter.type === "relation" && filter.field === relationListField.field);
|
|
106
|
+
if (existingRelationFilter && existingRelationFilter.type === "relation") {
|
|
107
|
+
if (existingRelationFilter.roles) {
|
|
108
|
+
const newRoles = new Set(existingRelationFilter.roles);
|
|
109
|
+
for (const role of relationListField.roles || noPreloadCacheRoles) {
|
|
110
|
+
if (noPreloadCacheRoles.includes(role)) {
|
|
111
|
+
newRoles.add(role);
|
|
112
|
+
}
|
|
89
113
|
}
|
|
114
|
+
existingRelationFilter.roles = Array.from(newRoles);
|
|
90
115
|
}
|
|
91
|
-
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
filters.push({
|
|
119
|
+
type: "relation",
|
|
120
|
+
field: relationListField.field,
|
|
121
|
+
roles: relationListField.roles?.filter((role) => noPreloadCacheRoles.includes(role)) ||
|
|
122
|
+
noPreloadCacheRoles,
|
|
123
|
+
});
|
|
92
124
|
}
|
|
93
125
|
}
|
|
94
|
-
else {
|
|
95
|
-
filters.push({
|
|
96
|
-
type: "relation",
|
|
97
|
-
field: relationListField.field,
|
|
98
|
-
roles: relationListField.roles?.filter((role) => noPreloadCacheRoles.includes(role)) ||
|
|
99
|
-
noPreloadCacheRoles,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
if (statusField || cards?.statusField) {
|
|
105
|
-
const existingStatusFieldIndex = filters.findIndex((filter) => "field" in filter && filter.field === (statusField?.field || cards?.statusField));
|
|
106
|
-
const newFilter = {
|
|
107
|
-
type: "status",
|
|
108
|
-
};
|
|
109
|
-
if (addUnscheduledRoles) {
|
|
110
|
-
newFilter.roles = calendar?.unscheduled?.roles;
|
|
111
126
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (existingRangeIndex === -1) {
|
|
120
|
-
if (noPreloadCacheRoles.length) {
|
|
121
|
-
filters.push({
|
|
122
|
-
type: "range",
|
|
123
|
-
field: calendar.startField,
|
|
124
|
-
});
|
|
127
|
+
if (statusField || cards?.statusField) {
|
|
128
|
+
const existingStatusFieldIndex = filters.findIndex((filter) => "field" in filter && filter.field === (statusField?.field || cards?.statusField));
|
|
129
|
+
const newFilter = {
|
|
130
|
+
type: "status",
|
|
131
|
+
};
|
|
132
|
+
if (addUnscheduledRoles) {
|
|
133
|
+
newFilter.roles = calendar?.unscheduled?.roles;
|
|
125
134
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
filters.splice(existingRangeIndex, 1);
|
|
135
|
+
filters.push(newFilter);
|
|
136
|
+
if (existingStatusFieldIndex !== -1) {
|
|
137
|
+
filters.splice(existingStatusFieldIndex, 1);
|
|
130
138
|
}
|
|
131
139
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const field = statusField?.field || cards?.statusField;
|
|
146
|
-
if (field) {
|
|
147
|
-
persistFilter.field = field;
|
|
140
|
+
if (calendar) {
|
|
141
|
+
const existingRangeIndex = filters.findIndex((filter) => filter.type === "range");
|
|
142
|
+
if (existingRangeIndex === -1) {
|
|
143
|
+
if (noPreloadCacheRoles.length) {
|
|
144
|
+
filters.push({
|
|
145
|
+
type: "range",
|
|
146
|
+
field: calendar.startField,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
if (noPreloadCacheRoles.length === 0) {
|
|
152
|
+
filters.splice(existingRangeIndex, 1);
|
|
148
153
|
}
|
|
149
154
|
}
|
|
150
155
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
+
if ((cards || admin.images) && standardRoles.length) {
|
|
157
|
+
filters.push({
|
|
158
|
+
type: "select",
|
|
159
|
+
field: "Last_Save_At",
|
|
160
|
+
roles: standardRoles,
|
|
161
|
+
});
|
|
156
162
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
163
|
+
const persistFilters = [];
|
|
164
|
+
filters.forEach((filter) => {
|
|
165
|
+
const persistFilter = {};
|
|
166
|
+
if (filter.type === "status") {
|
|
167
|
+
if (statusField || cards?.statusField) {
|
|
168
|
+
const field = statusField?.field || cards?.statusField;
|
|
169
|
+
if (field) {
|
|
170
|
+
persistFilter.field = field;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
160
173
|
}
|
|
161
174
|
else {
|
|
162
|
-
persistFilter.
|
|
175
|
+
persistFilter.field = filter.field;
|
|
163
176
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
177
|
+
if ("roles" in filter && filter.roles?.length) {
|
|
178
|
+
persistFilter.roles = filter.roles;
|
|
179
|
+
}
|
|
180
|
+
if (addUnscheduledRoles) {
|
|
181
|
+
if (persistFilter.roles?.length) {
|
|
182
|
+
persistFilter.roles = persistFilter.roles?.filter((role) => calendar.unscheduled?.roles?.includes(role));
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
persistFilter.roles = calendar.unscheduled?.roles;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (filter.type === "range") {
|
|
189
|
+
persistFilter.range = true;
|
|
190
|
+
}
|
|
191
|
+
if (filter.type !== "status" && filter.field === "Last_Save_At" && filter.type === "select") {
|
|
192
|
+
persistFilter.standalone = true;
|
|
193
|
+
}
|
|
194
|
+
persistFilters.push(persistFilter);
|
|
195
|
+
});
|
|
196
|
+
persistSchema.queries = persistFilters;
|
|
197
|
+
}
|
|
198
|
+
delete persistSchema.custom;
|
|
199
|
+
delete persistSchema.admin;
|
|
200
|
+
for (const field of persistSchema.fields) {
|
|
201
|
+
delete field.custom;
|
|
202
|
+
delete field.admin;
|
|
203
|
+
}
|
|
204
|
+
newSchema.collections[collection.split(".")[0]] = persistSchema;
|
|
174
205
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
206
|
+
return newSchema;
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
if (tempSchemaGenerated) {
|
|
210
|
+
const libPath = join(process.cwd(), "lib");
|
|
211
|
+
if (existsSync(libPath)) {
|
|
212
|
+
await rm(libPath, { recursive: true, force: true });
|
|
213
|
+
}
|
|
180
214
|
}
|
|
181
|
-
newSchema.collections[collection.split(".")[0]] = persistSchema;
|
|
182
215
|
}
|
|
183
|
-
return newSchema;
|
|
184
216
|
};
|
|
@@ -126,25 +126,22 @@ export const lintSchema = async (noLog = false) => {
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
for (const [collectionName, collectionSchema] of collectionSchemas) {
|
|
129
|
-
const { auth, fields, access, ttl, parentCollection, recordTitleField, softDelete, roleSystemFields, preloadCache, relationLists, fullTextSearch, ai, } = collectionSchema;
|
|
129
|
+
const { labels, auth, fields, access, ttl, parentCollection, recordTitleField, softDelete, roleSystemFields, preloadCache, relationLists, fullTextSearch, ai, } = collectionSchema;
|
|
130
130
|
const { auth: authAccess, operations, attributeRestrictions, entityRestrictions, permissionWriteRestrictions, serverReadOnly, serverWriteOnly, files, } = access;
|
|
131
131
|
// eslint-disable-next-line security/detect-object-injection
|
|
132
132
|
const customization = customizationModules[collectionName];
|
|
133
133
|
const readRoles = roles.filter((role) => roleHasOperationAccess(collectionSchema, role, "read"));
|
|
134
134
|
const fieldNames = fields.map((field) => field.name);
|
|
135
|
-
const
|
|
136
|
-
if (!
|
|
135
|
+
const firestoreRegex = /^(?!\/)(?!.*\/)(?!\.$)(?!\.\.$)(?!__.*__)[^/\s]{1,1500}$/;
|
|
136
|
+
if (!firestoreRegex.test(collectionName)) {
|
|
137
137
|
errors.push(`Invalid collection name: ${collectionName}. Must be a valid Firestore collection ID.`);
|
|
138
138
|
}
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
const tsIdentifierRegex = /^[A-Z][A-Za-z0-9_]*$/;
|
|
140
|
+
if (!tsIdentifierRegex.test(collectionName)) {
|
|
141
|
+
errors.push(`Invalid collection name: ${collectionName}. Must start with a capital letter and contain only letters, digits, and underscores.`);
|
|
141
142
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
errors.push(`Invalid collection name: ${collectionName}. Collection names should not have spaces and should start with a capital letter.`);
|
|
145
|
-
}
|
|
146
|
-
if (collectionName.includes("-")) {
|
|
147
|
-
errors.push(`Invalid collection name: ${collectionName}. Collection names cannot have dashes. Use underscores instead.`);
|
|
143
|
+
if (labels.collection !== collectionName) {
|
|
144
|
+
errors.push(`Invalid collection label: collection label "${labels.collection}" must match collection file name "${collectionName}".`);
|
|
148
145
|
}
|
|
149
146
|
fieldNames.forEach((fieldName) => {
|
|
150
147
|
const formattedFieldName = fieldName.replace(/\s+/g, "").replace(/^\w/, (c) => c.toUpperCase());
|
package/lib/src/main.js
CHANGED
|
@@ -18,6 +18,7 @@ if (!process.env.GCP_PROJECT &&
|
|
|
18
18
|
"generate-firestore-indexes",
|
|
19
19
|
"generate-firestore-rules",
|
|
20
20
|
"generate-storage-rules",
|
|
21
|
+
"generate-types",
|
|
21
22
|
"add-project",
|
|
22
23
|
"list-projects",
|
|
23
24
|
].includes(process.argv[2])) {
|
|
@@ -84,6 +85,7 @@ import { applySchema } from "./deploy/schema/applySchema.js";
|
|
|
84
85
|
import { addTenant } from "./project/addTenant.js";
|
|
85
86
|
import { deleteTenant } from "./project/deleteTenant.js";
|
|
86
87
|
import { updateLiveSchema } from "./deploy/schema/updateLiveSchema.js";
|
|
88
|
+
import { generateTypes } from "./types/generateTypes.js";
|
|
87
89
|
const program = new Command();
|
|
88
90
|
program.name("stoker").description("Stoker Platform CLI").version(pkg.version);
|
|
89
91
|
program
|
|
@@ -196,6 +198,12 @@ program
|
|
|
196
198
|
.action(() => {
|
|
197
199
|
generateStorageRules();
|
|
198
200
|
});
|
|
201
|
+
program
|
|
202
|
+
.command("generate-types")
|
|
203
|
+
.description("generate TypeScript collection types")
|
|
204
|
+
.action(() => {
|
|
205
|
+
generateTypes();
|
|
206
|
+
});
|
|
199
207
|
program
|
|
200
208
|
.command("deploy")
|
|
201
209
|
.description("deploy project")
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { writeFile } from "fs/promises";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { isRelationField } from "@stoker-platform/utils";
|
|
4
|
+
import { generateSchema } from "../deploy/schema/generateSchema.js";
|
|
5
|
+
const INDENT = " ";
|
|
6
|
+
const tsLiteral = (value) => JSON.stringify(value);
|
|
7
|
+
const literalUnion = (values) => values.map(tsLiteral).join(" | ");
|
|
8
|
+
const fieldType = (field, mode) => {
|
|
9
|
+
switch (field.type) {
|
|
10
|
+
case "String":
|
|
11
|
+
return field.values?.length ? literalUnion(field.values) : "string";
|
|
12
|
+
case "Boolean":
|
|
13
|
+
return "boolean";
|
|
14
|
+
case "Number":
|
|
15
|
+
if (field.values?.length)
|
|
16
|
+
return literalUnion(field.values);
|
|
17
|
+
if (field.autoIncrement) {
|
|
18
|
+
return mode === "record" ? `number | ${tsLiteral("Pending")}` : "number";
|
|
19
|
+
}
|
|
20
|
+
return "number";
|
|
21
|
+
case "Timestamp":
|
|
22
|
+
return "FirestoreTimestamp";
|
|
23
|
+
case "Array":
|
|
24
|
+
return field.values?.length ? `(${literalUnion(field.values)})[]` : "unknown[]";
|
|
25
|
+
case "Map":
|
|
26
|
+
return "Record<string, unknown>";
|
|
27
|
+
case "Embedding":
|
|
28
|
+
return "unknown";
|
|
29
|
+
case "Computed":
|
|
30
|
+
return mode === "record" ? "string | number" : "never";
|
|
31
|
+
default:
|
|
32
|
+
return "unknown";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const buildField = (field, mode) => {
|
|
36
|
+
if (field.type === "Embedding")
|
|
37
|
+
return [];
|
|
38
|
+
if (field.type === "Computed" && mode === "input")
|
|
39
|
+
return [];
|
|
40
|
+
if (isRelationField(field)) {
|
|
41
|
+
const required = Boolean(field.required);
|
|
42
|
+
const optional = required ? "" : "?";
|
|
43
|
+
if (mode === "input") {
|
|
44
|
+
return [`${INDENT}${tsLiteral(field.name)}${optional}: StokerRelationObject`];
|
|
45
|
+
}
|
|
46
|
+
return [
|
|
47
|
+
`${INDENT}${tsLiteral(field.name)}${optional}: StokerRelationObject`,
|
|
48
|
+
`${INDENT}${tsLiteral(`${field.name}_Array`)}${optional}: StokerRelationArray`,
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
const isAutoIncrement = field.type === "Number" && field.autoIncrement;
|
|
52
|
+
const required = Boolean(field.required) && !(mode === "input" && isAutoIncrement);
|
|
53
|
+
const optional = required ? "" : "?";
|
|
54
|
+
const nullable = "nullable" in field && field.nullable ? " | null" : "";
|
|
55
|
+
return [`${INDENT}${tsLiteral(field.name)}${optional}: ${fieldType(field, mode)}${nullable}`];
|
|
56
|
+
};
|
|
57
|
+
const buildRecordType = (collection) => {
|
|
58
|
+
const typeName = `${collection.labels.collection}Record`;
|
|
59
|
+
const lines = collection.fields.flatMap((field) => buildField(field, "record"));
|
|
60
|
+
return [`export type ${typeName} = SystemFields & {`, ...lines, "}"].join("\n");
|
|
61
|
+
};
|
|
62
|
+
const buildCreateInputType = (collection) => {
|
|
63
|
+
const typeName = `${collection.labels.collection}CreateInput`;
|
|
64
|
+
const lines = collection.fields.flatMap((field) => buildField(field, "input"));
|
|
65
|
+
return [`export type ${typeName} = {`, ...lines, "}"].join("\n");
|
|
66
|
+
};
|
|
67
|
+
const buildUpdateInputType = (collection) => {
|
|
68
|
+
const typeName = collection.labels.collection;
|
|
69
|
+
return `export type ${typeName}UpdateInput = Partial<${typeName}CreateInput>`;
|
|
70
|
+
};
|
|
71
|
+
const buildMap = (mapName, collections, suffix) => {
|
|
72
|
+
const entries = collections.map((collection) => `${INDENT}${tsLiteral(collection.labels.collection)}: ${collection.labels.collection}${suffix}`);
|
|
73
|
+
return [`export type ${mapName} = {`, ...entries, "}"].join("\n");
|
|
74
|
+
};
|
|
75
|
+
const buildOutput = (collections) => {
|
|
76
|
+
const sections = [
|
|
77
|
+
"import type {",
|
|
78
|
+
`${INDENT}FirestoreTimestamp,`,
|
|
79
|
+
`${INDENT}SystemFields,`,
|
|
80
|
+
`${INDENT}StokerRelationArray,`,
|
|
81
|
+
`${INDENT}StokerRelationObject,`,
|
|
82
|
+
'} from "@stoker-platform/types"',
|
|
83
|
+
"",
|
|
84
|
+
[
|
|
85
|
+
"export type CollectionName =",
|
|
86
|
+
...collections.map((collection) => `${INDENT}| ${tsLiteral(collection.labels.collection)}`),
|
|
87
|
+
].join("\n"),
|
|
88
|
+
"",
|
|
89
|
+
];
|
|
90
|
+
for (const collection of collections) {
|
|
91
|
+
sections.push(buildRecordType(collection), "", buildCreateInputType(collection), "", buildUpdateInputType(collection), "");
|
|
92
|
+
}
|
|
93
|
+
sections.push(buildMap("CollectionRecordMap", collections, "Record"), "", buildMap("CollectionCreateInputMap", collections, "CreateInput"), "", buildMap("CollectionUpdateInputMap", collections, "UpdateInput"), "", "export type CollectionRecord<C extends CollectionName> = CollectionRecordMap[C]", "export type CollectionCreateInput<C extends CollectionName> = CollectionCreateInputMap[C]", "export type CollectionUpdateInput<C extends CollectionName> = CollectionUpdateInputMap[C]", "");
|
|
94
|
+
return sections.join("\n");
|
|
95
|
+
};
|
|
96
|
+
export const generateTypes = async () => {
|
|
97
|
+
const schema = await generateSchema(true, true);
|
|
98
|
+
const collections = Object.values(schema.collections).sort((a, b) => a.labels.collection.localeCompare(b.labels.collection));
|
|
99
|
+
const outPath = join(process.cwd(), "src", "types.ts");
|
|
100
|
+
await writeFile(outPath, buildOutput(collections), "utf8");
|
|
101
|
+
if (collections.length === 1) {
|
|
102
|
+
console.log("Generated types for 1 collection");
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
console.log(`Generated types for ${collections.length} collections`);
|
|
106
|
+
}
|
|
107
|
+
};
|
package/lib/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["../src/main.ts","../src/data/exporttobigquery.ts","../src/data/seeddata.ts","../src/deploy/deployproject.ts","../src/deploy/cloud-functions/getfunctionsdata.ts","../src/deploy/firestore-export/exportfirestoredata.ts","../src/deploy/firestore-ttl/deployttls.ts","../src/deploy/live-update/liveupdate.ts","../src/deploy/maintenance/activatemaintenancemode.ts","../src/deploy/maintenance/disablemaintenancemode.ts","../src/deploy/maintenance/setdeploymentstatus.ts","../src/deploy/rules-indexes/generatefirestoreindexes.ts","../src/deploy/rules-indexes/generatefirestorerules.ts","../src/deploy/rules-indexes/generatestoragerules.ts","../src/deploy/schema/applyschema.ts","../src/deploy/schema/generateschema.ts","../src/deploy/schema/persistschema.ts","../src/deploy/schema/updateliveschema.ts","../src/lint/lintschema.ts","../src/lint/securityreport.ts","../src/migration/migrateall.ts","../src/migration/firestore/migratefirestore.ts","../src/migration/firestore/operations/deletefield.ts","../src/ops/auditdenormalized.ts","../src/ops/auditpermissions.ts","../src/ops/auditrelations.ts","../src/ops/explainpreloadqueries.ts","../src/ops/getuser.ts","../src/ops/getuserpermissions.ts","../src/ops/getuserrecord.ts","../src/ops/listprojects.ts","../src/ops/setusercollection.ts","../src/ops/setuserdocument.ts","../src/ops/setuserrole.ts","../src/project/addproject.ts","../src/project/addrecord.ts","../src/project/addrecordprompt.ts","../src/project/addtenant.ts","../src/project/buildwebapp.ts","../src/project/customdomain.ts","../src/project/deleteproject.ts","../src/project/deleterecord.ts","../src/project/deletetenant.ts","../src/project/getone.ts","../src/project/getsome.ts","../src/project/initproject.ts","../src/project/prepareemulatordata.ts","../src/project/setproject.ts","../src/project/startemulators.ts","../src/project/updaterecord.ts"],"version":"6.0.3"}
|
|
1
|
+
{"root":["../src/main.ts","../src/data/exporttobigquery.ts","../src/data/seeddata.ts","../src/deploy/deployproject.ts","../src/deploy/cloud-functions/getfunctionsdata.ts","../src/deploy/firestore-export/exportfirestoredata.ts","../src/deploy/firestore-ttl/deployttls.ts","../src/deploy/live-update/liveupdate.ts","../src/deploy/maintenance/activatemaintenancemode.ts","../src/deploy/maintenance/disablemaintenancemode.ts","../src/deploy/maintenance/setdeploymentstatus.ts","../src/deploy/rules-indexes/generatefirestoreindexes.ts","../src/deploy/rules-indexes/generatefirestorerules.ts","../src/deploy/rules-indexes/generatestoragerules.ts","../src/deploy/schema/applyschema.ts","../src/deploy/schema/generateschema.ts","../src/deploy/schema/persistschema.ts","../src/deploy/schema/updateliveschema.ts","../src/lint/lintschema.ts","../src/lint/securityreport.ts","../src/migration/migrateall.ts","../src/migration/firestore/migratefirestore.ts","../src/migration/firestore/operations/deletefield.ts","../src/ops/auditdenormalized.ts","../src/ops/auditpermissions.ts","../src/ops/auditrelations.ts","../src/ops/explainpreloadqueries.ts","../src/ops/getuser.ts","../src/ops/getuserpermissions.ts","../src/ops/getuserrecord.ts","../src/ops/listprojects.ts","../src/ops/setusercollection.ts","../src/ops/setuserdocument.ts","../src/ops/setuserrole.ts","../src/project/addproject.ts","../src/project/addrecord.ts","../src/project/addrecordprompt.ts","../src/project/addtenant.ts","../src/project/buildwebapp.ts","../src/project/customdomain.ts","../src/project/deleteproject.ts","../src/project/deleterecord.ts","../src/project/deletetenant.ts","../src/project/getone.ts","../src/project/getsome.ts","../src/project/initproject.ts","../src/project/prepareemulatordata.ts","../src/project/setproject.ts","../src/project/startemulators.ts","../src/project/updaterecord.ts","../src/types/generatetypes.ts"],"version":"6.0.3"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stoker-platform/cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.86",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"main": "./lib/src/main.js",
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"@google-cloud/secret-manager": "^6.1.1",
|
|
25
25
|
"@google-cloud/storage": "^7.19.0",
|
|
26
26
|
"@inquirer/prompts": "^8.4.2",
|
|
27
|
-
"@stoker-platform/node-client": "0.5.
|
|
28
|
-
"@stoker-platform/types": "0.5.
|
|
29
|
-
"@stoker-platform/utils": "0.5.
|
|
27
|
+
"@stoker-platform/node-client": "0.5.60",
|
|
28
|
+
"@stoker-platform/types": "0.5.39",
|
|
29
|
+
"@stoker-platform/utils": "0.5.51",
|
|
30
30
|
"algoliasearch": "^5.51.0",
|
|
31
31
|
"commander": "^14.0.0",
|
|
32
32
|
"cross-spawn": "^7.0.6",
|