rastack 0.0.63 → 0.0.64
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/CHANGELOG.md +7 -0
- package/admin/AdminApp.tsx +56 -3
- package/admin/index.tsx +8 -2
- package/admin/ui/icons.tsx +9 -0
- package/admin/ui/styles.ts +3 -0
- package/admin/useAdmin.ts +41 -4
- package/dist/admin.js +17 -14
- package/dist/compile/analyze.js +11 -1
- package/dist/define/index.d.ts +8 -1
- package/dist/define/index.js +2 -2
- package/dist/define/manifest.js +7 -1
- package/dist/deploy/io.d.ts +50 -0
- package/dist/deploy/io.js +124 -0
- package/dist/rastack-bootstrap.js +18 -47
- package/dist/rastack-init.d.ts +6 -3
- package/dist/rastack-init.js +43 -30
- package/dist/wasm/rastack_wasm.js +1 -1
- package/dist/wasm/rastack_wasm_bg.wasm +0 -0
- package/package.json +1 -1
- package/provider/provider.tsx +23 -0
- package/provider/types.ts +9 -0
- package/src/compile/analyze.ts +11 -1
- package/src/define/index.ts +5 -1
- package/src/define/manifest.ts +7 -1
- package/src/deploy/io.ts +162 -1
- package/src/rastack-bootstrap.ts +24 -69
- package/src/rastack-init.ts +50 -41
package/dist/compile/analyze.js
CHANGED
|
@@ -172,7 +172,17 @@ function analyzeField(name, initializer, checker) {
|
|
|
172
172
|
const app = literalTypeProp(fieldType, "__app", checker, initializer);
|
|
173
173
|
const model = literalTypeProp(fieldType, "__model", checker, initializer);
|
|
174
174
|
if (app && model) {
|
|
175
|
-
|
|
175
|
+
const fk = { name, type: "fk", relation: { app, model } };
|
|
176
|
+
// `s.ref(() => X, { null: true })` — a nullable relation (a shared secret
|
|
177
|
+
// with no project, an unassigned task). The options object is the ref
|
|
178
|
+
// call's second argument, read the same way scalar constraints are.
|
|
179
|
+
if (ts.isCallExpression(initializer)) {
|
|
180
|
+
const opts = literalToValue(initializer.arguments[1]);
|
|
181
|
+
if (opts && typeof opts === "object" && opts.null === true) {
|
|
182
|
+
fk.null = true;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return fk;
|
|
176
186
|
}
|
|
177
187
|
return undefined;
|
|
178
188
|
}
|
package/dist/define/index.d.ts
CHANGED
|
@@ -64,6 +64,11 @@ export interface Ref<App extends string, Model extends string> {
|
|
|
64
64
|
readonly __model: Model;
|
|
65
65
|
/** The authored `() => Resource` thunk — carried at runtime. */
|
|
66
66
|
readonly target?: () => ResourceType<App, Model, any>;
|
|
67
|
+
/** Optional FK constraints — `null: true` makes the relation nullable
|
|
68
|
+
* (e.g. a shared secret with no project, or an unassigned task). */
|
|
69
|
+
readonly options?: {
|
|
70
|
+
null?: boolean;
|
|
71
|
+
};
|
|
67
72
|
}
|
|
68
73
|
export interface StringOptions {
|
|
69
74
|
maxLength?: number;
|
|
@@ -105,7 +110,9 @@ export declare const s: {
|
|
|
105
110
|
* the relation from the *type* `Ref<App, Model>` this returns, and the
|
|
106
111
|
* runtime keeps the thunk so `manifestFromResources()` can resolve it too.
|
|
107
112
|
*/
|
|
108
|
-
ref<A extends string, M extends string>(thunk: () => ResourceType<A, M, any
|
|
113
|
+
ref<A extends string, M extends string>(thunk: () => ResourceType<A, M, any>, options?: {
|
|
114
|
+
null?: boolean;
|
|
115
|
+
}): Ref<A, M>;
|
|
109
116
|
};
|
|
110
117
|
export type AnyField = Scalar<string> | Ref<string, string>;
|
|
111
118
|
/** Local-first sync configuration (mirrors the old Python `class Sync:`). */
|
package/dist/define/index.js
CHANGED
|
@@ -56,8 +56,8 @@ exports.s = {
|
|
|
56
56
|
* the relation from the *type* `Ref<App, Model>` this returns, and the
|
|
57
57
|
* runtime keeps the thunk so `manifestFromResources()` can resolve it too.
|
|
58
58
|
*/
|
|
59
|
-
ref(thunk) {
|
|
60
|
-
return { __rastackRef: true, target: thunk };
|
|
59
|
+
ref(thunk, options) {
|
|
60
|
+
return { __rastackRef: true, target: thunk, options };
|
|
61
61
|
},
|
|
62
62
|
};
|
|
63
63
|
/** Define a resource. Capture app/model (and options) as literal types. */
|
package/dist/define/manifest.js
CHANGED
|
@@ -89,11 +89,17 @@ function runtimeField(res, name, value) {
|
|
|
89
89
|
if (!isResourceValue(target)) {
|
|
90
90
|
throw new Error(`manifestFromResources: ${res.app}.${res.model}.${name} is an s.ref(...) whose target thunk did not return a resource() value`);
|
|
91
91
|
}
|
|
92
|
-
|
|
92
|
+
const fk = {
|
|
93
93
|
name,
|
|
94
94
|
type: "fk",
|
|
95
95
|
relation: { app: target.app, model: target.model },
|
|
96
96
|
};
|
|
97
|
+
// A nullable FK (`s.ref(() => X, { null: true })`) — e.g. a shared secret
|
|
98
|
+
// with no project — so the engine allows and stores a null relation.
|
|
99
|
+
if (field.options?.null === true) {
|
|
100
|
+
fk.null = true;
|
|
101
|
+
}
|
|
102
|
+
return fk;
|
|
97
103
|
}
|
|
98
104
|
if (typeof field.__rastackScalar !== "string")
|
|
99
105
|
return undefined;
|
package/dist/deploy/io.d.ts
CHANGED
|
@@ -22,6 +22,27 @@ export declare function configPath(cwd?: string): string;
|
|
|
22
22
|
* repo itself still works. Returns null when no candidate exists.
|
|
23
23
|
*/
|
|
24
24
|
export declare function resolveDeployAsset(relPath: string, cwd?: string): string | null;
|
|
25
|
+
/**
|
|
26
|
+
* Base URL the framework's CloudFormation templates are published at. The same
|
|
27
|
+
* site that hosts the installer (`reactapistack.com/setup.sh`) serves the base
|
|
28
|
+
* templates, so a CLI whose bundled copy is missing (a partial install, or a
|
|
29
|
+
* globally-linked dev build) can still fetch them.
|
|
30
|
+
*/
|
|
31
|
+
export declare const TEMPLATE_BASE_URL = "https://reactapistack.com/cloudformation";
|
|
32
|
+
/**
|
|
33
|
+
* Download a published CloudFormation template to a temp file, returning its
|
|
34
|
+
* path — or null on any failure (offline, 404, write error). Used as the
|
|
35
|
+
* fallback when the template is not bundled with the install.
|
|
36
|
+
*/
|
|
37
|
+
export declare function downloadTemplate(name: string, base?: string): Promise<string | null>;
|
|
38
|
+
/**
|
|
39
|
+
* Resolve a CloudFormation template to a local file path for `aws cloudformation
|
|
40
|
+
* deploy` (which accepts only `--template-file`). Prefers the copy bundled with
|
|
41
|
+
* the `rastack` install; when that is missing, downloads it from the React API
|
|
42
|
+
* Stack website. Returns null only when the template is neither bundled nor
|
|
43
|
+
* fetchable.
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveTemplateFile(name: string, cwd?: string): Promise<string | null>;
|
|
25
46
|
/** Read + normalize the deploy config, or null when it does not exist yet. */
|
|
26
47
|
export declare function loadConfig(cwd?: string): DeployConfig | null;
|
|
27
48
|
/** Write the deploy config, creating `.rastack/` if needed. */
|
|
@@ -39,6 +60,14 @@ export declare function awsCapture(args: string[], cwd?: string): string | null;
|
|
|
39
60
|
export declare function awsRun(args: string[], cwd?: string): void;
|
|
40
61
|
/** True when the AWS CLI is on PATH. */
|
|
41
62
|
export declare function awsAvailable(): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* The AWS account id the CLI's active credentials resolve to, or null when the
|
|
65
|
+
* CLI has no usable credentials (not logged in, an expired SSO session, or no
|
|
66
|
+
* configured profile). `init`/`bootstrap` call this to fail fast with an
|
|
67
|
+
* actionable "log in" message before attempting a stack deploy, instead of
|
|
68
|
+
* letting the first `cloudformation deploy` fail with a cryptic AWS error.
|
|
69
|
+
*/
|
|
70
|
+
export declare function awsIdentity(region?: string): string | null;
|
|
42
71
|
/**
|
|
43
72
|
* A single Output value of a CloudFormation stack, or null when the stack does
|
|
44
73
|
* not exist or the key is unset. `describe-stacks` prints `None` for a missing
|
|
@@ -52,6 +81,27 @@ export declare function stackOutput(stackName: string, key: string, region: stri
|
|
|
52
81
|
* on every `init`. Returns false when the placeholder file is missing.
|
|
53
82
|
*/
|
|
54
83
|
export declare function uploadPlaceholder(artifactsBucket: string, region: string, cwd?: string): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Deploy the tier-0 bootstrap stack, retrying once against an account that
|
|
86
|
+
* already has a GitHub OIDC provider — AWS allows only one provider per URL, so
|
|
87
|
+
* a second create is rejected and the retry tells the template to reuse the
|
|
88
|
+
* existing one. Throws on failure (after the retry). Shared by `rastack init`
|
|
89
|
+
* and `rastack bootstrap` so both stand the stack up identically.
|
|
90
|
+
*/
|
|
91
|
+
export declare function deployBootstrap(cfg: DeployConfig, reuseOidc: boolean, template: string): void;
|
|
92
|
+
/**
|
|
93
|
+
* Ensure the tier-0 bootstrap stack exists for this account+region, **creating
|
|
94
|
+
* it when missing**, and return its artifacts/templates buckets. This is why
|
|
95
|
+
* `rastack init` is a single command that stands up *everything* — the
|
|
96
|
+
* account-level bootstrap stack and the per-app backend — with no separate
|
|
97
|
+
* `rastack bootstrap` step. Idempotent: an existing stack is reconciled in
|
|
98
|
+
* place. Throws with an actionable message when the template cannot be resolved,
|
|
99
|
+
* the deploy fails, or the buckets can't be read back.
|
|
100
|
+
*/
|
|
101
|
+
export declare function ensureBootstrapStack(cfg: DeployConfig, reuseOidc?: boolean): Promise<{
|
|
102
|
+
artifactsBucket: string;
|
|
103
|
+
templatesBucket: string | null;
|
|
104
|
+
}>;
|
|
55
105
|
/** True when the GitHub CLI is on PATH. */
|
|
56
106
|
export declare function ghAvailable(): boolean;
|
|
57
107
|
/**
|
package/dist/deploy/io.js
CHANGED
|
@@ -38,8 +38,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
38
38
|
};
|
|
39
39
|
})();
|
|
40
40
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.TEMPLATE_BASE_URL = void 0;
|
|
41
42
|
exports.configPath = configPath;
|
|
42
43
|
exports.resolveDeployAsset = resolveDeployAsset;
|
|
44
|
+
exports.downloadTemplate = downloadTemplate;
|
|
45
|
+
exports.resolveTemplateFile = resolveTemplateFile;
|
|
43
46
|
exports.loadConfig = loadConfig;
|
|
44
47
|
exports.saveConfig = saveConfig;
|
|
45
48
|
exports.gitRemoteUrl = gitRemoteUrl;
|
|
@@ -47,8 +50,11 @@ exports.canPrompt = canPrompt;
|
|
|
47
50
|
exports.awsCapture = awsCapture;
|
|
48
51
|
exports.awsRun = awsRun;
|
|
49
52
|
exports.awsAvailable = awsAvailable;
|
|
53
|
+
exports.awsIdentity = awsIdentity;
|
|
50
54
|
exports.stackOutput = stackOutput;
|
|
51
55
|
exports.uploadPlaceholder = uploadPlaceholder;
|
|
56
|
+
exports.deployBootstrap = deployBootstrap;
|
|
57
|
+
exports.ensureBootstrapStack = ensureBootstrapStack;
|
|
52
58
|
exports.ghAvailable = ghAvailable;
|
|
53
59
|
exports.ghAuthenticated = ghAuthenticated;
|
|
54
60
|
exports.ghAuthLogin = ghAuthLogin;
|
|
@@ -58,6 +64,7 @@ exports.ask = ask;
|
|
|
58
64
|
exports.withPrompt = withPrompt;
|
|
59
65
|
const child_process_1 = require("child_process");
|
|
60
66
|
const fs = __importStar(require("fs"));
|
|
67
|
+
const https = __importStar(require("https"));
|
|
61
68
|
const os = __importStar(require("os"));
|
|
62
69
|
const path = __importStar(require("path"));
|
|
63
70
|
const readline = __importStar(require("readline"));
|
|
@@ -97,6 +104,54 @@ function resolveDeployAsset(relPath, cwd = process.cwd()) {
|
|
|
97
104
|
}
|
|
98
105
|
return null;
|
|
99
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Base URL the framework's CloudFormation templates are published at. The same
|
|
109
|
+
* site that hosts the installer (`reactapistack.com/setup.sh`) serves the base
|
|
110
|
+
* templates, so a CLI whose bundled copy is missing (a partial install, or a
|
|
111
|
+
* globally-linked dev build) can still fetch them.
|
|
112
|
+
*/
|
|
113
|
+
exports.TEMPLATE_BASE_URL = "https://reactapistack.com/cloudformation";
|
|
114
|
+
/**
|
|
115
|
+
* Download a published CloudFormation template to a temp file, returning its
|
|
116
|
+
* path — or null on any failure (offline, 404, write error). Used as the
|
|
117
|
+
* fallback when the template is not bundled with the install.
|
|
118
|
+
*/
|
|
119
|
+
function downloadTemplate(name, base = exports.TEMPLATE_BASE_URL) {
|
|
120
|
+
const url = `${base}/${name}`;
|
|
121
|
+
return new Promise((resolve) => {
|
|
122
|
+
https
|
|
123
|
+
.get(url, (res) => {
|
|
124
|
+
if (res.statusCode !== 200) {
|
|
125
|
+
res.resume();
|
|
126
|
+
resolve(null);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const chunks = [];
|
|
130
|
+
res.on("data", (c) => chunks.push(c));
|
|
131
|
+
res.on("end", () => {
|
|
132
|
+
try {
|
|
133
|
+
const dest = path.join(os.tmpdir(), `rastack-${path.basename(name)}`);
|
|
134
|
+
fs.writeFileSync(dest, Buffer.concat(chunks));
|
|
135
|
+
resolve(dest);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
resolve(null);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
})
|
|
142
|
+
.on("error", () => resolve(null));
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Resolve a CloudFormation template to a local file path for `aws cloudformation
|
|
147
|
+
* deploy` (which accepts only `--template-file`). Prefers the copy bundled with
|
|
148
|
+
* the `rastack` install; when that is missing, downloads it from the React API
|
|
149
|
+
* Stack website. Returns null only when the template is neither bundled nor
|
|
150
|
+
* fetchable.
|
|
151
|
+
*/
|
|
152
|
+
async function resolveTemplateFile(name, cwd = process.cwd()) {
|
|
153
|
+
return resolveDeployAsset(name, cwd) ?? (await downloadTemplate(name));
|
|
154
|
+
}
|
|
100
155
|
/** Read + normalize the deploy config, or null when it does not exist yet. */
|
|
101
156
|
function loadConfig(cwd = process.cwd()) {
|
|
102
157
|
const file = configPath(cwd);
|
|
@@ -163,6 +218,26 @@ function awsRun(args, cwd = process.cwd()) {
|
|
|
163
218
|
function awsAvailable() {
|
|
164
219
|
return awsCapture(["--version"]) !== null;
|
|
165
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* The AWS account id the CLI's active credentials resolve to, or null when the
|
|
223
|
+
* CLI has no usable credentials (not logged in, an expired SSO session, or no
|
|
224
|
+
* configured profile). `init`/`bootstrap` call this to fail fast with an
|
|
225
|
+
* actionable "log in" message before attempting a stack deploy, instead of
|
|
226
|
+
* letting the first `cloudformation deploy` fail with a cryptic AWS error.
|
|
227
|
+
*/
|
|
228
|
+
function awsIdentity(region) {
|
|
229
|
+
const args = [
|
|
230
|
+
"sts",
|
|
231
|
+
"get-caller-identity",
|
|
232
|
+
"--query",
|
|
233
|
+
"Account",
|
|
234
|
+
"--output",
|
|
235
|
+
"text",
|
|
236
|
+
];
|
|
237
|
+
if (region)
|
|
238
|
+
args.push("--region", region);
|
|
239
|
+
return awsCapture(args);
|
|
240
|
+
}
|
|
166
241
|
/**
|
|
167
242
|
* A single Output value of a CloudFormation stack, or null when the stack does
|
|
168
243
|
* not exist or the key is unset. `describe-stacks` prints `None` for a missing
|
|
@@ -210,6 +285,55 @@ function uploadPlaceholder(artifactsBucket, region, cwd = process.cwd()) {
|
|
|
210
285
|
]);
|
|
211
286
|
return true;
|
|
212
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Deploy the tier-0 bootstrap stack, retrying once against an account that
|
|
290
|
+
* already has a GitHub OIDC provider — AWS allows only one provider per URL, so
|
|
291
|
+
* a second create is rejected and the retry tells the template to reuse the
|
|
292
|
+
* existing one. Throws on failure (after the retry). Shared by `rastack init`
|
|
293
|
+
* and `rastack bootstrap` so both stand the stack up identically.
|
|
294
|
+
*/
|
|
295
|
+
function deployBootstrap(cfg, reuseOidc, template) {
|
|
296
|
+
try {
|
|
297
|
+
awsRun((0, config_1.bootstrapStackArgs)(cfg, reuseOidc, template));
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
if (reuseOidc)
|
|
301
|
+
throw err;
|
|
302
|
+
console.log(" • retrying bootstrap, reusing the account's existing GitHub OIDC provider");
|
|
303
|
+
awsRun((0, config_1.bootstrapStackArgs)(cfg, true, template));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Ensure the tier-0 bootstrap stack exists for this account+region, **creating
|
|
308
|
+
* it when missing**, and return its artifacts/templates buckets. This is why
|
|
309
|
+
* `rastack init` is a single command that stands up *everything* — the
|
|
310
|
+
* account-level bootstrap stack and the per-app backend — with no separate
|
|
311
|
+
* `rastack bootstrap` step. Idempotent: an existing stack is reconciled in
|
|
312
|
+
* place. Throws with an actionable message when the template cannot be resolved,
|
|
313
|
+
* the deploy fails, or the buckets can't be read back.
|
|
314
|
+
*/
|
|
315
|
+
async function ensureBootstrapStack(cfg, reuseOidc = false) {
|
|
316
|
+
const stack = (0, config_1.bootstrapStackName)(cfg);
|
|
317
|
+
const existing = stackOutput(stack, config_1.BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region);
|
|
318
|
+
if (existing) {
|
|
319
|
+
console.log(` ✓ bootstrap stack ${stack} already exists — reconciling`);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
console.log(`\n▸ Creating the tier-0 bootstrap stack ${stack} (@ ${cfg.region})`);
|
|
323
|
+
}
|
|
324
|
+
const template = await resolveTemplateFile("bootstrap.yaml");
|
|
325
|
+
if (!template) {
|
|
326
|
+
throw new Error("bootstrap.yaml template not found in the rastack package and could not be\n" +
|
|
327
|
+
" downloaded from reactapistack.com. Reinstall rastack (`npm i -g rastack`).");
|
|
328
|
+
}
|
|
329
|
+
deployBootstrap(cfg, reuseOidc, template);
|
|
330
|
+
const artifactsBucket = stackOutput(stack, config_1.BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region);
|
|
331
|
+
const templatesBucket = stackOutput(stack, config_1.BOOTSTRAP_TEMPLATES_OUTPUT, cfg.region);
|
|
332
|
+
if (!artifactsBucket) {
|
|
333
|
+
throw new Error(`bootstrap stack ${stack} deployed but its ArtifactsBucket output could not be read.`);
|
|
334
|
+
}
|
|
335
|
+
return { artifactsBucket, templatesBucket };
|
|
336
|
+
}
|
|
213
337
|
// ── GitHub CLI boundary ──────────────────────────────────────────────────────
|
|
214
338
|
// After the stack is up `init` wires the two repo Variables CI reads
|
|
215
339
|
// (AWS_DEPLOY_ROLE_ARN / AWS_REGION) through the developer's own `gh` — already
|
|
@@ -124,60 +124,31 @@ function printPlan(cfg, reuseOidc) {
|
|
|
124
124
|
" ArtifactsBucket to create app backends. Nothing here is app-specific.\n");
|
|
125
125
|
}
|
|
126
126
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
127
|
+
* Stand the tier-0 bootstrap stack up and upload the Lambda placeholder. The
|
|
128
|
+
* deploy (with the GitHub-OIDC-reuse retry) and template resolution are shared
|
|
129
|
+
* with `rastack init` via `ensureBootstrapStack`, so both stand the stack up
|
|
130
|
+
* identically.
|
|
130
131
|
*/
|
|
131
|
-
function
|
|
132
|
-
const template = (0, io_1.resolveDeployAsset)("bootstrap.yaml");
|
|
133
|
-
if (!template) {
|
|
134
|
-
console.error(" ✗ bootstrap.yaml template not found in the rastack package. Reinstall\n" +
|
|
135
|
-
" rastack (`npm i -g rastack`) or run from the framework repo.");
|
|
136
|
-
process.exit(1);
|
|
137
|
-
}
|
|
138
|
-
try {
|
|
139
|
-
(0, io_1.awsRun)((0, config_1.bootstrapStackArgs)(cfg, reuseOidc, template));
|
|
140
|
-
}
|
|
141
|
-
catch (err) {
|
|
142
|
-
if (reuseOidc) {
|
|
143
|
-
console.error(`\n ✗ bootstrap stack deploy failed.\n ${err.message}`);
|
|
144
|
-
process.exit(1);
|
|
145
|
-
}
|
|
146
|
-
// The most common cause is an account that already has a GitHub OIDC
|
|
147
|
-
// provider. Retry telling the template to reuse it instead of creating a
|
|
148
|
-
// second (which CloudFormation rejects).
|
|
149
|
-
console.log(" • retrying bootstrap, reusing the account's existing GitHub OIDC provider");
|
|
150
|
-
try {
|
|
151
|
-
(0, io_1.awsRun)((0, config_1.bootstrapStackArgs)(cfg, true, template));
|
|
152
|
-
}
|
|
153
|
-
catch (retryErr) {
|
|
154
|
-
console.error(`\n ✗ bootstrap stack deploy failed.\n ${retryErr.message}`);
|
|
155
|
-
process.exit(1);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
/** Stand the tier-0 bootstrap stack up and upload the Lambda placeholder. */
|
|
160
|
-
function standUp(cfg, reuseOidc) {
|
|
132
|
+
async function standUp(cfg, reuseOidc) {
|
|
161
133
|
if (!(0, io_1.awsAvailable)()) {
|
|
162
134
|
console.error(" ✗ the AWS CLI is not on PATH. Install it and configure credentials,\n" +
|
|
163
135
|
" or re-run with --print to see the command.");
|
|
164
136
|
process.exit(1);
|
|
165
137
|
}
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
if (
|
|
169
|
-
console.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
console.log(`\n▸ Creating bootstrap stack ${stack} (@ ${cfg.region})`);
|
|
173
|
-
}
|
|
174
|
-
deployBootstrap(cfg, reuseOidc);
|
|
175
|
-
const artifactsBucket = (0, io_1.stackOutput)(stack, config_1.BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region);
|
|
176
|
-
const templatesBucket = (0, io_1.stackOutput)(stack, config_1.BOOTSTRAP_TEMPLATES_OUTPUT, cfg.region);
|
|
177
|
-
if (!artifactsBucket) {
|
|
178
|
-
console.error(" ✗ bootstrap stack deployed but its ArtifactsBucket output could not be read.");
|
|
138
|
+
// Verify the AWS CLI is logged in before attempting a stack deploy.
|
|
139
|
+
const account = (0, io_1.awsIdentity)(cfg.region);
|
|
140
|
+
if (!account) {
|
|
141
|
+
console.error(" ✗ the AWS CLI has no usable credentials — it is not logged in.\n" +
|
|
142
|
+
" Run `aws configure` (or `aws sso login`), then re-run `rastack bootstrap`.\n" +
|
|
143
|
+
" Or re-run with --print to see the command.");
|
|
179
144
|
process.exit(1);
|
|
180
145
|
}
|
|
146
|
+
console.log(` ✓ AWS account ${account} (${cfg.region})`);
|
|
147
|
+
const stack = (0, config_1.bootstrapStackName)(cfg);
|
|
148
|
+
const { artifactsBucket, templatesBucket } = await (0, io_1.ensureBootstrapStack)(cfg, reuseOidc).catch((err) => {
|
|
149
|
+
console.error(`\n ✗ ${err.message}`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
});
|
|
181
152
|
console.log(` ✓ artifacts bucket: ${artifactsBucket}`);
|
|
182
153
|
if (templatesBucket)
|
|
183
154
|
console.log(` ✓ templates bucket: ${templatesBucket}`);
|
|
@@ -220,7 +191,7 @@ async function main(argv) {
|
|
|
220
191
|
return;
|
|
221
192
|
}
|
|
222
193
|
}
|
|
223
|
-
standUp(cfg, flags.reuseOidc);
|
|
194
|
+
await standUp(cfg, flags.reuseOidc);
|
|
224
195
|
}
|
|
225
196
|
if (require.main === module) {
|
|
226
197
|
main(process.argv.slice(2)).catch((err) => {
|
package/dist/rastack-init.d.ts
CHANGED
|
@@ -11,14 +11,17 @@
|
|
|
11
11
|
* 1. writes `.rastack/deploy.json` + `.github/workflows/{ci,deploy-backend}.yml`
|
|
12
12
|
* (GitHub coordinates default from the `origin` remote; missing fields are
|
|
13
13
|
* prompted for on a TTY);
|
|
14
|
-
* 2.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* 2. verifies the AWS CLI is logged in, then ensures the tier-0 bootstrap stack
|
|
15
|
+
* exists — creating it (the S3 template/placeholder buckets + GitHub OIDC
|
|
16
|
+
* provider) when missing, reconciling it when it already does;
|
|
17
17
|
* 3. uploads the Lambda placeholder bundle to that bucket;
|
|
18
18
|
* 4. deploys the per-app backend (app.yaml) with the resolved ArtifactsBucket;
|
|
19
19
|
* 5. reads the stack Outputs back into the config and prints the two GitHub repo
|
|
20
20
|
* Variables CI needs.
|
|
21
21
|
*
|
|
22
|
+
* The CloudFormation templates ship inside the `rastack` package; a partial
|
|
23
|
+
* install falls back to downloading them from reactapistack.com.
|
|
24
|
+
*
|
|
22
25
|
* Thereafter the recurring code deploy is CI's job — `rastack ci deploy`, which
|
|
23
26
|
* `rastack deploy` also runs. `--no-deploy` scaffolds only; `--print` shows the
|
|
24
27
|
* exact single-line `aws cloudformation deploy` commands without running them.
|
package/dist/rastack-init.js
CHANGED
|
@@ -12,14 +12,17 @@
|
|
|
12
12
|
* 1. writes `.rastack/deploy.json` + `.github/workflows/{ci,deploy-backend}.yml`
|
|
13
13
|
* (GitHub coordinates default from the `origin` remote; missing fields are
|
|
14
14
|
* prompted for on a TTY);
|
|
15
|
-
* 2.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* 2. verifies the AWS CLI is logged in, then ensures the tier-0 bootstrap stack
|
|
16
|
+
* exists — creating it (the S3 template/placeholder buckets + GitHub OIDC
|
|
17
|
+
* provider) when missing, reconciling it when it already does;
|
|
18
18
|
* 3. uploads the Lambda placeholder bundle to that bucket;
|
|
19
19
|
* 4. deploys the per-app backend (app.yaml) with the resolved ArtifactsBucket;
|
|
20
20
|
* 5. reads the stack Outputs back into the config and prints the two GitHub repo
|
|
21
21
|
* Variables CI needs.
|
|
22
22
|
*
|
|
23
|
+
* The CloudFormation templates ship inside the `rastack` package; a partial
|
|
24
|
+
* install falls back to downloading them from reactapistack.com.
|
|
25
|
+
*
|
|
23
26
|
* Thereafter the recurring code deploy is CI's job — `rastack ci deploy`, which
|
|
24
27
|
* `rastack deploy` also runs. `--no-deploy` scaffolds only; `--print` shows the
|
|
25
28
|
* exact single-line `aws cloudformation deploy` commands without running them.
|
|
@@ -244,8 +247,10 @@ function envLabel(cfg, env) {
|
|
|
244
247
|
}
|
|
245
248
|
/**
|
|
246
249
|
* Print the exact commands `init` would run, without touching the cloud
|
|
247
|
-
* (`--print`).
|
|
248
|
-
*
|
|
250
|
+
* (`--print`). init stands up *both* stacks, so both `aws cloudformation deploy`
|
|
251
|
+
* commands are shown: the tier-0 bootstrap stack (created only when missing) and
|
|
252
|
+
* the per-app backend. The artifacts bucket is read from an existing bootstrap
|
|
253
|
+
* stack when the AWS CLI is available, else shown as a placeholder.
|
|
249
254
|
*/
|
|
250
255
|
function printPlan(cfg, envs) {
|
|
251
256
|
const bucket = (0, io_1.awsAvailable)()
|
|
@@ -254,11 +259,12 @@ function printPlan(cfg, envs) {
|
|
|
254
259
|
const withBucket = (0, config_1.mergeConfig)(cfg, {
|
|
255
260
|
artifactsBucket: bucket ?? "<ArtifactsBucketName from bootstrap stack>",
|
|
256
261
|
});
|
|
262
|
+
const bootstrapTemplate = (0, io_1.resolveDeployAsset)("bootstrap.yaml") ?? undefined;
|
|
257
263
|
const appTemplate = (0, io_1.resolveDeployAsset)("app.yaml") ?? undefined;
|
|
258
264
|
console.log(`\n Deployment plan (${envs.join(" + ")} @ ${cfg.region})\n`);
|
|
259
|
-
console.log(` ──
|
|
260
|
-
console.log(
|
|
261
|
-
console.log(" ──
|
|
265
|
+
console.log(` ── 1. Tier-0 bootstrap stack (${(0, config_1.bootstrapStackName)(cfg)}) — created only when missing ──`);
|
|
266
|
+
console.log("\n " + (0, config_1.bootstrapStackCommand)(cfg, false, bootstrapTemplate) + "\n");
|
|
267
|
+
console.log(" ── 2. Per-app backend (app.yaml) — one stack per environment ──");
|
|
262
268
|
for (const env of envs) {
|
|
263
269
|
const e = (0, config_1.resolveEnvironment)(withBucket, env);
|
|
264
270
|
console.log(`\n # ${envLabel(withBucket, env)} — stack ${e.stackName}`);
|
|
@@ -276,9 +282,12 @@ function printPlan(cfg, envs) {
|
|
|
276
282
|
console.log("");
|
|
277
283
|
}
|
|
278
284
|
/**
|
|
279
|
-
* Stand the infrastructure up:
|
|
280
|
-
*
|
|
281
|
-
*
|
|
285
|
+
* Stand the infrastructure up: verify the AWS CLI is logged in, ensure the
|
|
286
|
+
* tier-0 bootstrap stack (creating it when missing), upload the Lambda
|
|
287
|
+
* placeholder, deploy app.yaml, and read the Outputs back into the config.
|
|
288
|
+
* `init` is the single command that stands up *everything* — there is no
|
|
289
|
+
* separate manual bootstrap step. Returns the config enriched with the resolved
|
|
290
|
+
* buckets + outputs.
|
|
282
291
|
*/
|
|
283
292
|
async function standUp(cfg, envs, interactive) {
|
|
284
293
|
if (!(0, io_1.awsAvailable)()) {
|
|
@@ -286,23 +295,27 @@ async function standUp(cfg, envs, interactive) {
|
|
|
286
295
|
" or re-run with --print to see the commands, or --no-deploy to skip.");
|
|
287
296
|
process.exit(1);
|
|
288
297
|
}
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
console.error(`\n ✗ Bootstrap stack ${(0, config_1.bootstrapStackName)(cfg)} not found in ${cfg.region}.\n` +
|
|
300
|
-
" It is the once-per-account+region step that hosts the CloudFormation\n" +
|
|
301
|
-
" templates and the Lambda placeholder every app deploy reads. Create it\n" +
|
|
302
|
-
" first (an account admin does this once), then re-run `rastack init`:\n\n" +
|
|
303
|
-
` rastack bootstrap --region ${cfg.region}\n`);
|
|
298
|
+
// Verify the AWS CLI is actually logged in before creating any stacks, so a
|
|
299
|
+
// missing/expired credential fails fast with an actionable message rather than
|
|
300
|
+
// a cryptic `cloudformation deploy` error partway through.
|
|
301
|
+
const account = (0, io_1.awsIdentity)(cfg.region);
|
|
302
|
+
if (!account) {
|
|
303
|
+
console.error(" ✗ the AWS CLI has no usable credentials — it is not logged in.\n" +
|
|
304
|
+
" Configure credentials, then re-run `rastack init`:\n" +
|
|
305
|
+
" aws configure # static access key / secret\n" +
|
|
306
|
+
" aws sso login # or an existing SSO session\n" +
|
|
307
|
+
" Or re-run with --print to see the commands, or --no-deploy to skip.");
|
|
304
308
|
process.exit(1);
|
|
305
309
|
}
|
|
310
|
+
console.log(` ✓ AWS account ${account} (${cfg.region})`);
|
|
311
|
+
// 1. Ensure the tier-0 bootstrap stack exists — the S3 buckets that host the
|
|
312
|
+
// templates + Lambda placeholder and the GitHub OIDC provider. init creates
|
|
313
|
+
// it when missing (idempotent for an existing one), so a single command
|
|
314
|
+
// takes an account+repo from nothing to a deployed backend.
|
|
315
|
+
const { artifactsBucket, templatesBucket } = await (0, io_1.ensureBootstrapStack)(cfg).catch((err) => {
|
|
316
|
+
console.error(`\n ✗ ${err.message}`);
|
|
317
|
+
process.exit(1);
|
|
318
|
+
});
|
|
306
319
|
console.log(` ✓ artifacts bucket: ${artifactsBucket}`);
|
|
307
320
|
// 2. Upload the placeholder the function is created from (shared by all envs).
|
|
308
321
|
console.log("\n▸ Uploading the Lambda placeholder bundle");
|
|
@@ -314,10 +327,10 @@ async function standUp(cfg, envs, interactive) {
|
|
|
314
327
|
artifactsBucket,
|
|
315
328
|
templatesBucket: templatesBucket ?? undefined,
|
|
316
329
|
});
|
|
317
|
-
const appTemplate = (0, io_1.
|
|
330
|
+
const appTemplate = await (0, io_1.resolveTemplateFile)("app.yaml");
|
|
318
331
|
if (!appTemplate) {
|
|
319
|
-
console.error(" ✗ app.yaml template not found in the rastack package
|
|
320
|
-
" (`npm i -g rastack`)
|
|
332
|
+
console.error(" ✗ app.yaml template not found in the rastack package and could not be\n" +
|
|
333
|
+
" downloaded from reactapistack.com. Reinstall rastack (`npm i -g rastack`).");
|
|
321
334
|
process.exit(1);
|
|
322
335
|
}
|
|
323
336
|
// 3. Deploy one stack per environment — each with its own Cognito pool,
|
|
@@ -501,7 +514,7 @@ async function main(argv) {
|
|
|
501
514
|
const stacks = flags.envs
|
|
502
515
|
.map((env) => (0, config_1.resolveEnvironment)(cfg, env).stackName)
|
|
503
516
|
.join(" + ");
|
|
504
|
-
const ok = await confirm(`Deploy ${stacks} to AWS ${cfg.region} now? (
|
|
517
|
+
const ok = await confirm(`Deploy ${stacks} to AWS ${cfg.region} now? (creates the bootstrap stack if missing) [y/N]`);
|
|
505
518
|
if (!ok) {
|
|
506
519
|
console.log("\n Skipped the deploy. Re-run to deploy, or `rastack init --print` for the commands.\n");
|
|
507
520
|
return;
|
|
@@ -375,7 +375,7 @@ function __wbg_get_imports() {
|
|
|
375
375
|
return ret;
|
|
376
376
|
},
|
|
377
377
|
__wbindgen_cast_0000000000000001: function(arg0, arg1) {
|
|
378
|
-
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx:
|
|
378
|
+
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 90, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
|
|
379
379
|
const ret = makeMutClosure(arg0, arg1, wasm_bindgen_ac0f9960a408a037___convert__closures_____invoke___wasm_bindgen_ac0f9960a408a037___JsValue__core_9b3796e30d99ddb7___result__Result_____wasm_bindgen_ac0f9960a408a037___JsError___true_);
|
|
380
380
|
return ret;
|
|
381
381
|
},
|
|
Binary file
|
package/package.json
CHANGED
package/provider/provider.tsx
CHANGED
|
@@ -170,6 +170,11 @@ export function RAStackProvider({
|
|
|
170
170
|
const existing = await persistence.load();
|
|
171
171
|
if (existing) {
|
|
172
172
|
await engine.importWarehouse(existing);
|
|
173
|
+
} else if (config.seedWarehouse) {
|
|
174
|
+
// Seed from a bundled warehouse blob (exportWarehouse format) rather
|
|
175
|
+
// than loose Iceberg files.
|
|
176
|
+
const seed = await config.seedWarehouse();
|
|
177
|
+
if (seed) await engine.importWarehouse(seed);
|
|
173
178
|
} else {
|
|
174
179
|
const base = config.warehouseBaseUrl ?? "/data/warehouse/";
|
|
175
180
|
const resources = manifestResources(manifest);
|
|
@@ -208,6 +213,24 @@ export function RAStackProvider({
|
|
|
208
213
|
}, 400);
|
|
209
214
|
persistRef.current = persist;
|
|
210
215
|
|
|
216
|
+
// Persist at the *engine* level, not just in the axios adapter. Consumers
|
|
217
|
+
// that talk to the engine directly (the admin console calls
|
|
218
|
+
// `engine.handle("DELETE", …)` straight, bypassing the generated hooks /
|
|
219
|
+
// axios seam) must still have their writes saved to IndexedDB — otherwise
|
|
220
|
+
// every reload re-seeds from the committed warehouse and silently
|
|
221
|
+
// resurrects deleted rows. Wrapping `handle` here makes persistence fire
|
|
222
|
+
// for both paths (the adapter's `onWrite` below then coalesces into the
|
|
223
|
+
// same debounce, so an app-hook write persists exactly once).
|
|
224
|
+
const MUTATING = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
225
|
+
const rawHandle = engine.handle.bind(engine);
|
|
226
|
+
engine.handle = ((method: string, path: string, query?: any, body?: any) =>
|
|
227
|
+
Promise.resolve(rawHandle(method, path, query, body)).then((res: any) => {
|
|
228
|
+
if (MUTATING.has(String(method).toUpperCase()) && res && res.status < 300) {
|
|
229
|
+
persist();
|
|
230
|
+
}
|
|
231
|
+
return res;
|
|
232
|
+
})) as typeof engine.handle;
|
|
233
|
+
|
|
211
234
|
engineRef.current = engine;
|
|
212
235
|
configureApi({
|
|
213
236
|
baseURL: "",
|
package/provider/types.ts
CHANGED
|
@@ -152,6 +152,15 @@ export interface RAStackProviderConfig {
|
|
|
152
152
|
fileSource?: import("../cache").WarehouseFileSource;
|
|
153
153
|
/** `s3` mode: how to load/save the warehouse blob from object storage. */
|
|
154
154
|
warehouse?: WarehouseSource;
|
|
155
|
+
/**
|
|
156
|
+
* Seed `local` mode from a **bundled warehouse blob** — the format
|
|
157
|
+
* `exportWarehouse()` produces and `importWarehouse()` consumes — instead of
|
|
158
|
+
* walking loose Iceberg files under `warehouseBaseUrl`. Use this when the app
|
|
159
|
+
* commits a single warehouse blob asset (e.g. bundled JSON) rather than a
|
|
160
|
+
* directory of parquet files. Takes precedence over `warehouseBaseUrl` /
|
|
161
|
+
* `fileSource` seeding, and is ignored once a persisted local copy exists.
|
|
162
|
+
*/
|
|
163
|
+
seedWarehouse?: () => Uint8Array | null | Promise<Uint8Array | null>;
|
|
155
164
|
/**
|
|
156
165
|
* Override how the WASM module is loaded. Supply this from app code (where
|
|
157
166
|
* your bundler can resolve the generated bundle) if the default loader can't
|