@stardeck-customer-apps/compose 0.3.0 → 0.4.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/SKILL.md +2 -0
- package/dist/cli.js +67 -11
- package/dist/index.d.mts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +67 -11
- package/dist/index.mjs +67 -11
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -40,6 +40,8 @@ from `apps/web/stardeck.compose.json` instead:
|
|
|
40
40
|
```jsonc
|
|
41
41
|
{
|
|
42
42
|
"connectedStores": [
|
|
43
|
+
// `type` is optional and defaults to "database"; "storage" marks an
|
|
44
|
+
// object-storage store. The platform writes database rows only for now.
|
|
43
45
|
{ "storeId": "ds_123", "slug": "media", "bindingKey": null, "accessLevel": "write" }
|
|
44
46
|
],
|
|
45
47
|
"installs": {
|
package/dist/cli.js
CHANGED
|
@@ -239,6 +239,14 @@ var datastoreRequirementSchema = import_zod.z.object({
|
|
|
239
239
|
id: tableNameSchema,
|
|
240
240
|
/** "app" = this project's app-local store; "org" = an org-level shared store. */
|
|
241
241
|
scope: import_zod.z.enum(["app", "org"]).default("app"),
|
|
242
|
+
/**
|
|
243
|
+
* Which kind of connected store satisfies this requirement. A "database"
|
|
244
|
+
* requirement only ever binds to a database store and a "storage" one
|
|
245
|
+
* (file/object storage — uploads, images) only to a storage store; the
|
|
246
|
+
* rail never crosses the two. Defaults to "database", the only kind the
|
|
247
|
+
* rail resolved before this field existed.
|
|
248
|
+
*/
|
|
249
|
+
type: import_zod.z.enum(["database", "storage"]).default("database"),
|
|
242
250
|
/** true → install fails when no store resolves; false → binding omitted, module degrades. */
|
|
243
251
|
required: import_zod.z.boolean().default(true),
|
|
244
252
|
/** Checked against the connected store's accessLevel at install. */
|
|
@@ -2080,7 +2088,11 @@ var composeInputsSchema = import_zod7.z.strictObject({
|
|
|
2080
2088
|
// A row with an empty binding key must not fail a deploy: the rail
|
|
2081
2089
|
// treats falsy as "no binding key", so normalise rather than reject.
|
|
2082
2090
|
bindingKey: import_zod7.z.string().nullish().transform((value) => value || null),
|
|
2083
|
-
accessLevel: import_zod7.z.enum(["read", "write", "admin"])
|
|
2091
|
+
accessLevel: import_zod7.z.enum(["read", "write", "admin"]),
|
|
2092
|
+
// Defaulted so a file written before the platform emitted store types
|
|
2093
|
+
// (database rows only) still parses. The platform starts writing
|
|
2094
|
+
// storage rows, with `type` set, once forks run a compose that reads it.
|
|
2095
|
+
type: import_zod7.z.enum(["database", "storage"]).default("database")
|
|
2084
2096
|
})
|
|
2085
2097
|
).default([]),
|
|
2086
2098
|
installs: import_zod7.z.record(
|
|
@@ -2091,6 +2103,17 @@ var composeInputsSchema = import_zod7.z.strictObject({
|
|
|
2091
2103
|
})
|
|
2092
2104
|
).default({})
|
|
2093
2105
|
});
|
|
2106
|
+
var STUB_IGNORE_PATH = "apps/web/src/app/.gitignore";
|
|
2107
|
+
var STUB_IGNORE_HEADER = "# Generated by stardeck-compose \u2014 Module-rail stubs are build outputs; do not edit or commit them.";
|
|
2108
|
+
function escapeStubIgnorePattern(pattern) {
|
|
2109
|
+
const escaped = pattern.replace(/[\\[\]*?]/g, (char) => `\\${char}`);
|
|
2110
|
+
return /^[#!]/.test(escaped) ? `\\${escaped}` : escaped;
|
|
2111
|
+
}
|
|
2112
|
+
function buildStubIgnore(artifactPaths) {
|
|
2113
|
+
const prefix = "apps/web/src/app/";
|
|
2114
|
+
const entries = artifactPaths.filter((artifact) => artifact.startsWith(prefix)).map((artifact) => escapeStubIgnorePattern(`/${artifact.slice(prefix.length)}`)).sort();
|
|
2115
|
+
return [STUB_IGNORE_HEADER, ...entries].join("\n") + "\n";
|
|
2116
|
+
}
|
|
2094
2117
|
|
|
2095
2118
|
// ../../packages/lib/src/shared/module-enablement.ts
|
|
2096
2119
|
function isModuleEnabledByList(raw, name, kindOf) {
|
|
@@ -3585,7 +3608,9 @@ function dirnamePosix(filePath) {
|
|
|
3585
3608
|
// ../../packages/lib/src/server/module-rail/compose-inputs.ts
|
|
3586
3609
|
async function buildComposeInputs(input) {
|
|
3587
3610
|
const { db, projectId, deps } = input;
|
|
3588
|
-
const connectedStores = await deps.
|
|
3611
|
+
const connectedStores = (await deps.listConnectedStoresForProject(db, projectId)).filter(
|
|
3612
|
+
(store) => store.type === "database"
|
|
3613
|
+
);
|
|
3589
3614
|
const installRows = await Promise.all(
|
|
3590
3615
|
input.moduleNames.map(async (moduleName) => ({
|
|
3591
3616
|
moduleName,
|
|
@@ -3600,7 +3625,11 @@ async function buildComposeInputs(input) {
|
|
|
3600
3625
|
datastoreBindings: install.datastoreBindings ?? null
|
|
3601
3626
|
};
|
|
3602
3627
|
}
|
|
3603
|
-
|
|
3628
|
+
const parsed = composeInputsSchema.parse({ connectedStores, installs });
|
|
3629
|
+
return {
|
|
3630
|
+
...parsed,
|
|
3631
|
+
connectedStores: parsed.connectedStores.map(({ type: _type, ...store }) => store)
|
|
3632
|
+
};
|
|
3604
3633
|
}
|
|
3605
3634
|
|
|
3606
3635
|
// ../../packages/lib/src/shared/data-store-manifest.ts
|
|
@@ -3626,7 +3655,7 @@ function compareStores(a, b) {
|
|
|
3626
3655
|
function describeCandidates(stores) {
|
|
3627
3656
|
if (stores.length === 0) return "none";
|
|
3628
3657
|
return stores.map(
|
|
3629
|
-
(store) => `${store.storeId} (slug=${store.slug}, bindingKey=${store.bindingKey ?? "none"}, access=${store.accessLevel})`
|
|
3658
|
+
(store) => `${store.storeId} (type=${store.type}, slug=${store.slug}, bindingKey=${store.bindingKey ?? "none"}, access=${store.accessLevel})`
|
|
3630
3659
|
).join(", ");
|
|
3631
3660
|
}
|
|
3632
3661
|
function groupStoresById(stores) {
|
|
@@ -3657,14 +3686,15 @@ function accessLevelForBinding(group, binding) {
|
|
|
3657
3686
|
);
|
|
3658
3687
|
}
|
|
3659
3688
|
function resolveModuleDataStoreBindings(input) {
|
|
3660
|
-
const
|
|
3661
|
-
const storeGroups = groupStoresById(stores);
|
|
3689
|
+
const allStores = [...input.connectedStores].sort(compareStores);
|
|
3662
3690
|
const requirements = [...input.requirements].sort((a, b) => compareStrings(a.id, b.id));
|
|
3663
3691
|
const overrides = input.explicitOverrides ?? {};
|
|
3664
3692
|
const bindings = {};
|
|
3665
3693
|
const errors = [];
|
|
3666
|
-
const candidates = describeCandidates(stores);
|
|
3667
3694
|
for (const requirement of requirements) {
|
|
3695
|
+
const stores = allStores.filter((store) => store.type === requirement.type);
|
|
3696
|
+
const storeGroups = groupStoresById(stores);
|
|
3697
|
+
const candidates = describeCandidates(allStores);
|
|
3668
3698
|
const logicalSlug = dataStoreSlug(requirement.id);
|
|
3669
3699
|
let selected;
|
|
3670
3700
|
if (Object.prototype.hasOwnProperty.call(overrides, requirement.id)) {
|
|
@@ -3677,7 +3707,7 @@ function resolveModuleDataStoreBindings(input) {
|
|
|
3677
3707
|
moduleName: input.moduleName,
|
|
3678
3708
|
logicalId: requirement.id,
|
|
3679
3709
|
kind: "explicit-override",
|
|
3680
|
-
message: matches.length === 0 ? `Module "${input.moduleName}" data store "${requirement.id}" override "${override}" did not match a connected
|
|
3710
|
+
message: matches.length === 0 ? `Module "${input.moduleName}" data store "${requirement.id}" override "${override}" did not match a connected ${requirement.type} store; candidates: ${candidates}` : `Module "${input.moduleName}" data store "${requirement.id}" override "${override}" is ambiguous; matches: ${describeCandidates(matches.flatMap((group) => group.stores))}`
|
|
3681
3711
|
});
|
|
3682
3712
|
continue;
|
|
3683
3713
|
}
|
|
@@ -3688,6 +3718,8 @@ function resolveModuleDataStoreBindings(input) {
|
|
|
3688
3718
|
);
|
|
3689
3719
|
if (conventionMatches.length === 1) {
|
|
3690
3720
|
selected = conventionMatches[0];
|
|
3721
|
+
} else if (conventionMatches.length === 0 && requirement.type === "storage") {
|
|
3722
|
+
if (storeGroups.length === 1) selected = storeGroups[0];
|
|
3691
3723
|
} else if (conventionMatches.length === 0 && requirement.scope === "app") {
|
|
3692
3724
|
if (storeGroups.length === 1) {
|
|
3693
3725
|
selected = storeGroups[0];
|
|
@@ -4046,7 +4078,7 @@ async function resolveCompositionDataStoreBindings(context, presentNames, manife
|
|
|
4046
4078
|
if (modulesWithRequirements.length === 0) return bindingsByModule;
|
|
4047
4079
|
const deps = context.deps;
|
|
4048
4080
|
if (!deps) throw new Error("Module rail dependencies are required for data-store bindings");
|
|
4049
|
-
const connectedStores = await deps.
|
|
4081
|
+
const connectedStores = await deps.listConnectedStoresForProject(context.db, context.projectId);
|
|
4050
4082
|
const installs = await Promise.all(
|
|
4051
4083
|
modulesWithRequirements.map(
|
|
4052
4084
|
(moduleName) => deps.getModuleInstall(context.db, context.projectId, moduleName)
|
|
@@ -4336,6 +4368,21 @@ async function writeComposeInputs(target) {
|
|
|
4336
4368
|
);
|
|
4337
4369
|
return written;
|
|
4338
4370
|
}
|
|
4371
|
+
async function readStubIgnore(context) {
|
|
4372
|
+
const existing = await context.sandbox.fileExists(STUB_IGNORE_PATH) ? await context.sandbox.readFile(STUB_IGNORE_PATH) : null;
|
|
4373
|
+
if (existing !== null && existing.split("\n", 1)[0] !== STUB_IGNORE_HEADER) {
|
|
4374
|
+
throw new Error(
|
|
4375
|
+
`[ModuleRail] refusing to overwrite ${STUB_IGNORE_PATH}: it is not compose-generated (line 1 is not the generated header)`
|
|
4376
|
+
);
|
|
4377
|
+
}
|
|
4378
|
+
return existing;
|
|
4379
|
+
}
|
|
4380
|
+
async function writeStubIgnore(context, existing, artifactPaths) {
|
|
4381
|
+
const content = buildStubIgnore(artifactPaths);
|
|
4382
|
+
const written = existing === null || existing.trimEnd() !== content.trimEnd();
|
|
4383
|
+
if (written) await writeSandboxFileAtomic(context, STUB_IGNORE_PATH, content);
|
|
4384
|
+
return written;
|
|
4385
|
+
}
|
|
4339
4386
|
async function applyCompositionArtifactMutations(context, plan, existingFiles) {
|
|
4340
4387
|
const appliedWrites = [];
|
|
4341
4388
|
const appliedDeletes = [];
|
|
@@ -5200,9 +5247,17 @@ async function reconcileCompositionArtifacts(context, options = {}) {
|
|
|
5200
5247
|
deps
|
|
5201
5248
|
});
|
|
5202
5249
|
}
|
|
5250
|
+
const maintainIgnore = !options.dryRun && !options.enabledModulesRaw;
|
|
5251
|
+
const existingIgnore = maintainIgnore ? await readStubIgnore(context) : null;
|
|
5203
5252
|
if (changed && !options.dryRun) {
|
|
5204
5253
|
await applyCompositionArtifactMutations(context, planned.plan, existingFiles);
|
|
5205
5254
|
}
|
|
5255
|
+
if (maintainIgnore) {
|
|
5256
|
+
const wroteIgnore = await writeStubIgnore(context, existingIgnore, planned.plan.desiredPaths);
|
|
5257
|
+
console.log(
|
|
5258
|
+
`[ModuleRail] runId=${context.runId} stub ignore list entries=${planned.plan.desiredPaths.length} written=${wroteIgnore}`
|
|
5259
|
+
);
|
|
5260
|
+
}
|
|
5206
5261
|
return {
|
|
5207
5262
|
registryPath: planned.plan.registryPath,
|
|
5208
5263
|
writePaths: planned.plan.writes.map((w) => w.path),
|
|
@@ -5239,7 +5294,8 @@ function readComposeInputs(cwd) {
|
|
|
5239
5294
|
storeId: store.storeId,
|
|
5240
5295
|
slug: store.slug,
|
|
5241
5296
|
bindingKey: store.bindingKey ?? null,
|
|
5242
|
-
accessLevel: store.accessLevel
|
|
5297
|
+
accessLevel: store.accessLevel,
|
|
5298
|
+
type: store.type
|
|
5243
5299
|
})),
|
|
5244
5300
|
installs: parsed.data.installs
|
|
5245
5301
|
};
|
|
@@ -5373,7 +5429,7 @@ async function compose(options) {
|
|
|
5373
5429
|
const deps = {
|
|
5374
5430
|
sandbox,
|
|
5375
5431
|
getModuleInstall: async (_db, _projectId, moduleName) => inputs.installs[moduleName] ?? null,
|
|
5376
|
-
|
|
5432
|
+
listConnectedStoresForProject: async () => inputs.connectedStores
|
|
5377
5433
|
};
|
|
5378
5434
|
const context = {
|
|
5379
5435
|
db: {},
|
package/dist/index.d.mts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
/** A data store connected to a project, as the composition rail consumes it. */
|
|
3
|
+
/** A data store (database or storage) connected to a project, as the composition rail consumes it. */
|
|
4
4
|
type ConnectedStore = {
|
|
5
5
|
storeId: string;
|
|
6
6
|
slug: string;
|
|
7
7
|
bindingKey: string | null;
|
|
8
8
|
accessLevel: "read" | "write" | "admin";
|
|
9
|
+
type: "database" | "storage";
|
|
9
10
|
};
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -30,6 +31,10 @@ declare const composeInputsSchema: z.ZodObject<{
|
|
|
30
31
|
write: "write";
|
|
31
32
|
admin: "admin";
|
|
32
33
|
}>;
|
|
34
|
+
type: z.ZodDefault<z.ZodEnum<{
|
|
35
|
+
database: "database";
|
|
36
|
+
storage: "storage";
|
|
37
|
+
}>>;
|
|
33
38
|
}, z.core.$strict>>>;
|
|
34
39
|
installs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
35
40
|
installedVersion: z.ZodString;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
/** A data store connected to a project, as the composition rail consumes it. */
|
|
3
|
+
/** A data store (database or storage) connected to a project, as the composition rail consumes it. */
|
|
4
4
|
type ConnectedStore = {
|
|
5
5
|
storeId: string;
|
|
6
6
|
slug: string;
|
|
7
7
|
bindingKey: string | null;
|
|
8
8
|
accessLevel: "read" | "write" | "admin";
|
|
9
|
+
type: "database" | "storage";
|
|
9
10
|
};
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -30,6 +31,10 @@ declare const composeInputsSchema: z.ZodObject<{
|
|
|
30
31
|
write: "write";
|
|
31
32
|
admin: "admin";
|
|
32
33
|
}>;
|
|
34
|
+
type: z.ZodDefault<z.ZodEnum<{
|
|
35
|
+
database: "database";
|
|
36
|
+
storage: "storage";
|
|
37
|
+
}>>;
|
|
33
38
|
}, z.core.$strict>>>;
|
|
34
39
|
installs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
35
40
|
installedVersion: z.ZodString;
|
package/dist/index.js
CHANGED
|
@@ -314,6 +314,14 @@ var datastoreRequirementSchema = import_zod.z.object({
|
|
|
314
314
|
id: tableNameSchema,
|
|
315
315
|
/** "app" = this project's app-local store; "org" = an org-level shared store. */
|
|
316
316
|
scope: import_zod.z.enum(["app", "org"]).default("app"),
|
|
317
|
+
/**
|
|
318
|
+
* Which kind of connected store satisfies this requirement. A "database"
|
|
319
|
+
* requirement only ever binds to a database store and a "storage" one
|
|
320
|
+
* (file/object storage — uploads, images) only to a storage store; the
|
|
321
|
+
* rail never crosses the two. Defaults to "database", the only kind the
|
|
322
|
+
* rail resolved before this field existed.
|
|
323
|
+
*/
|
|
324
|
+
type: import_zod.z.enum(["database", "storage"]).default("database"),
|
|
317
325
|
/** true → install fails when no store resolves; false → binding omitted, module degrades. */
|
|
318
326
|
required: import_zod.z.boolean().default(true),
|
|
319
327
|
/** Checked against the connected store's accessLevel at install. */
|
|
@@ -2078,7 +2086,11 @@ var composeInputsSchema = import_zod7.z.strictObject({
|
|
|
2078
2086
|
// A row with an empty binding key must not fail a deploy: the rail
|
|
2079
2087
|
// treats falsy as "no binding key", so normalise rather than reject.
|
|
2080
2088
|
bindingKey: import_zod7.z.string().nullish().transform((value) => value || null),
|
|
2081
|
-
accessLevel: import_zod7.z.enum(["read", "write", "admin"])
|
|
2089
|
+
accessLevel: import_zod7.z.enum(["read", "write", "admin"]),
|
|
2090
|
+
// Defaulted so a file written before the platform emitted store types
|
|
2091
|
+
// (database rows only) still parses. The platform starts writing
|
|
2092
|
+
// storage rows, with `type` set, once forks run a compose that reads it.
|
|
2093
|
+
type: import_zod7.z.enum(["database", "storage"]).default("database")
|
|
2082
2094
|
})
|
|
2083
2095
|
).default([]),
|
|
2084
2096
|
installs: import_zod7.z.record(
|
|
@@ -2089,6 +2101,17 @@ var composeInputsSchema = import_zod7.z.strictObject({
|
|
|
2089
2101
|
})
|
|
2090
2102
|
).default({})
|
|
2091
2103
|
});
|
|
2104
|
+
var STUB_IGNORE_PATH = "apps/web/src/app/.gitignore";
|
|
2105
|
+
var STUB_IGNORE_HEADER = "# Generated by stardeck-compose \u2014 Module-rail stubs are build outputs; do not edit or commit them.";
|
|
2106
|
+
function escapeStubIgnorePattern(pattern) {
|
|
2107
|
+
const escaped = pattern.replace(/[\\[\]*?]/g, (char) => `\\${char}`);
|
|
2108
|
+
return /^[#!]/.test(escaped) ? `\\${escaped}` : escaped;
|
|
2109
|
+
}
|
|
2110
|
+
function buildStubIgnore(artifactPaths) {
|
|
2111
|
+
const prefix = "apps/web/src/app/";
|
|
2112
|
+
const entries = artifactPaths.filter((artifact) => artifact.startsWith(prefix)).map((artifact) => escapeStubIgnorePattern(`/${artifact.slice(prefix.length)}`)).sort();
|
|
2113
|
+
return [STUB_IGNORE_HEADER, ...entries].join("\n") + "\n";
|
|
2114
|
+
}
|
|
2092
2115
|
|
|
2093
2116
|
// ../../packages/lib/src/shared/module-enablement.ts
|
|
2094
2117
|
function isModuleEnabledByList(raw, name, kindOf) {
|
|
@@ -3594,7 +3617,9 @@ function dirnamePosix(filePath) {
|
|
|
3594
3617
|
// ../../packages/lib/src/server/module-rail/compose-inputs.ts
|
|
3595
3618
|
async function buildComposeInputs(input) {
|
|
3596
3619
|
const { db, projectId, deps } = input;
|
|
3597
|
-
const connectedStores = await deps.
|
|
3620
|
+
const connectedStores = (await deps.listConnectedStoresForProject(db, projectId)).filter(
|
|
3621
|
+
(store) => store.type === "database"
|
|
3622
|
+
);
|
|
3598
3623
|
const installRows = await Promise.all(
|
|
3599
3624
|
input.moduleNames.map(async (moduleName) => ({
|
|
3600
3625
|
moduleName,
|
|
@@ -3609,7 +3634,11 @@ async function buildComposeInputs(input) {
|
|
|
3609
3634
|
datastoreBindings: install.datastoreBindings ?? null
|
|
3610
3635
|
};
|
|
3611
3636
|
}
|
|
3612
|
-
|
|
3637
|
+
const parsed = composeInputsSchema.parse({ connectedStores, installs });
|
|
3638
|
+
return {
|
|
3639
|
+
...parsed,
|
|
3640
|
+
connectedStores: parsed.connectedStores.map(({ type: _type, ...store }) => store)
|
|
3641
|
+
};
|
|
3613
3642
|
}
|
|
3614
3643
|
|
|
3615
3644
|
// ../../packages/lib/src/shared/data-store-manifest.ts
|
|
@@ -3635,7 +3664,7 @@ function compareStores(a, b) {
|
|
|
3635
3664
|
function describeCandidates(stores) {
|
|
3636
3665
|
if (stores.length === 0) return "none";
|
|
3637
3666
|
return stores.map(
|
|
3638
|
-
(store) => `${store.storeId} (slug=${store.slug}, bindingKey=${store.bindingKey ?? "none"}, access=${store.accessLevel})`
|
|
3667
|
+
(store) => `${store.storeId} (type=${store.type}, slug=${store.slug}, bindingKey=${store.bindingKey ?? "none"}, access=${store.accessLevel})`
|
|
3639
3668
|
).join(", ");
|
|
3640
3669
|
}
|
|
3641
3670
|
function groupStoresById(stores) {
|
|
@@ -3666,14 +3695,15 @@ function accessLevelForBinding(group, binding) {
|
|
|
3666
3695
|
);
|
|
3667
3696
|
}
|
|
3668
3697
|
function resolveModuleDataStoreBindings(input) {
|
|
3669
|
-
const
|
|
3670
|
-
const storeGroups = groupStoresById(stores);
|
|
3698
|
+
const allStores = [...input.connectedStores].sort(compareStores);
|
|
3671
3699
|
const requirements = [...input.requirements].sort((a, b) => compareStrings(a.id, b.id));
|
|
3672
3700
|
const overrides = input.explicitOverrides ?? {};
|
|
3673
3701
|
const bindings = {};
|
|
3674
3702
|
const errors = [];
|
|
3675
|
-
const candidates = describeCandidates(stores);
|
|
3676
3703
|
for (const requirement of requirements) {
|
|
3704
|
+
const stores = allStores.filter((store) => store.type === requirement.type);
|
|
3705
|
+
const storeGroups = groupStoresById(stores);
|
|
3706
|
+
const candidates = describeCandidates(allStores);
|
|
3677
3707
|
const logicalSlug = dataStoreSlug(requirement.id);
|
|
3678
3708
|
let selected;
|
|
3679
3709
|
if (Object.prototype.hasOwnProperty.call(overrides, requirement.id)) {
|
|
@@ -3686,7 +3716,7 @@ function resolveModuleDataStoreBindings(input) {
|
|
|
3686
3716
|
moduleName: input.moduleName,
|
|
3687
3717
|
logicalId: requirement.id,
|
|
3688
3718
|
kind: "explicit-override",
|
|
3689
|
-
message: matches.length === 0 ? `Module "${input.moduleName}" data store "${requirement.id}" override "${override}" did not match a connected
|
|
3719
|
+
message: matches.length === 0 ? `Module "${input.moduleName}" data store "${requirement.id}" override "${override}" did not match a connected ${requirement.type} store; candidates: ${candidates}` : `Module "${input.moduleName}" data store "${requirement.id}" override "${override}" is ambiguous; matches: ${describeCandidates(matches.flatMap((group) => group.stores))}`
|
|
3690
3720
|
});
|
|
3691
3721
|
continue;
|
|
3692
3722
|
}
|
|
@@ -3697,6 +3727,8 @@ function resolveModuleDataStoreBindings(input) {
|
|
|
3697
3727
|
);
|
|
3698
3728
|
if (conventionMatches.length === 1) {
|
|
3699
3729
|
selected = conventionMatches[0];
|
|
3730
|
+
} else if (conventionMatches.length === 0 && requirement.type === "storage") {
|
|
3731
|
+
if (storeGroups.length === 1) selected = storeGroups[0];
|
|
3700
3732
|
} else if (conventionMatches.length === 0 && requirement.scope === "app") {
|
|
3701
3733
|
if (storeGroups.length === 1) {
|
|
3702
3734
|
selected = storeGroups[0];
|
|
@@ -4055,7 +4087,7 @@ async function resolveCompositionDataStoreBindings(context, presentNames, manife
|
|
|
4055
4087
|
if (modulesWithRequirements.length === 0) return bindingsByModule;
|
|
4056
4088
|
const deps = context.deps;
|
|
4057
4089
|
if (!deps) throw new Error("Module rail dependencies are required for data-store bindings");
|
|
4058
|
-
const connectedStores = await deps.
|
|
4090
|
+
const connectedStores = await deps.listConnectedStoresForProject(context.db, context.projectId);
|
|
4059
4091
|
const installs = await Promise.all(
|
|
4060
4092
|
modulesWithRequirements.map(
|
|
4061
4093
|
(moduleName) => deps.getModuleInstall(context.db, context.projectId, moduleName)
|
|
@@ -4345,6 +4377,21 @@ async function writeComposeInputs(target) {
|
|
|
4345
4377
|
);
|
|
4346
4378
|
return written;
|
|
4347
4379
|
}
|
|
4380
|
+
async function readStubIgnore(context) {
|
|
4381
|
+
const existing = await context.sandbox.fileExists(STUB_IGNORE_PATH) ? await context.sandbox.readFile(STUB_IGNORE_PATH) : null;
|
|
4382
|
+
if (existing !== null && existing.split("\n", 1)[0] !== STUB_IGNORE_HEADER) {
|
|
4383
|
+
throw new Error(
|
|
4384
|
+
`[ModuleRail] refusing to overwrite ${STUB_IGNORE_PATH}: it is not compose-generated (line 1 is not the generated header)`
|
|
4385
|
+
);
|
|
4386
|
+
}
|
|
4387
|
+
return existing;
|
|
4388
|
+
}
|
|
4389
|
+
async function writeStubIgnore(context, existing, artifactPaths) {
|
|
4390
|
+
const content = buildStubIgnore(artifactPaths);
|
|
4391
|
+
const written = existing === null || existing.trimEnd() !== content.trimEnd();
|
|
4392
|
+
if (written) await writeSandboxFileAtomic(context, STUB_IGNORE_PATH, content);
|
|
4393
|
+
return written;
|
|
4394
|
+
}
|
|
4348
4395
|
async function applyCompositionArtifactMutations(context, plan, existingFiles) {
|
|
4349
4396
|
const appliedWrites = [];
|
|
4350
4397
|
const appliedDeletes = [];
|
|
@@ -5209,9 +5256,17 @@ async function reconcileCompositionArtifacts(context, options = {}) {
|
|
|
5209
5256
|
deps
|
|
5210
5257
|
});
|
|
5211
5258
|
}
|
|
5259
|
+
const maintainIgnore = !options.dryRun && !options.enabledModulesRaw;
|
|
5260
|
+
const existingIgnore = maintainIgnore ? await readStubIgnore(context) : null;
|
|
5212
5261
|
if (changed && !options.dryRun) {
|
|
5213
5262
|
await applyCompositionArtifactMutations(context, planned.plan, existingFiles);
|
|
5214
5263
|
}
|
|
5264
|
+
if (maintainIgnore) {
|
|
5265
|
+
const wroteIgnore = await writeStubIgnore(context, existingIgnore, planned.plan.desiredPaths);
|
|
5266
|
+
console.log(
|
|
5267
|
+
`[ModuleRail] runId=${context.runId} stub ignore list entries=${planned.plan.desiredPaths.length} written=${wroteIgnore}`
|
|
5268
|
+
);
|
|
5269
|
+
}
|
|
5215
5270
|
return {
|
|
5216
5271
|
registryPath: planned.plan.registryPath,
|
|
5217
5272
|
writePaths: planned.plan.writes.map((w) => w.path),
|
|
@@ -5248,7 +5303,8 @@ function readComposeInputs(cwd) {
|
|
|
5248
5303
|
storeId: store.storeId,
|
|
5249
5304
|
slug: store.slug,
|
|
5250
5305
|
bindingKey: store.bindingKey ?? null,
|
|
5251
|
-
accessLevel: store.accessLevel
|
|
5306
|
+
accessLevel: store.accessLevel,
|
|
5307
|
+
type: store.type
|
|
5252
5308
|
})),
|
|
5253
5309
|
installs: parsed.data.installs
|
|
5254
5310
|
};
|
|
@@ -5382,7 +5438,7 @@ async function compose(options) {
|
|
|
5382
5438
|
const deps = {
|
|
5383
5439
|
sandbox,
|
|
5384
5440
|
getModuleInstall: async (_db, _projectId, moduleName) => inputs.installs[moduleName] ?? null,
|
|
5385
|
-
|
|
5441
|
+
listConnectedStoresForProject: async () => inputs.connectedStores
|
|
5386
5442
|
};
|
|
5387
5443
|
const context = {
|
|
5388
5444
|
db: {},
|
package/dist/index.mjs
CHANGED
|
@@ -276,6 +276,14 @@ var datastoreRequirementSchema = z.object({
|
|
|
276
276
|
id: tableNameSchema,
|
|
277
277
|
/** "app" = this project's app-local store; "org" = an org-level shared store. */
|
|
278
278
|
scope: z.enum(["app", "org"]).default("app"),
|
|
279
|
+
/**
|
|
280
|
+
* Which kind of connected store satisfies this requirement. A "database"
|
|
281
|
+
* requirement only ever binds to a database store and a "storage" one
|
|
282
|
+
* (file/object storage — uploads, images) only to a storage store; the
|
|
283
|
+
* rail never crosses the two. Defaults to "database", the only kind the
|
|
284
|
+
* rail resolved before this field existed.
|
|
285
|
+
*/
|
|
286
|
+
type: z.enum(["database", "storage"]).default("database"),
|
|
279
287
|
/** true → install fails when no store resolves; false → binding omitted, module degrades. */
|
|
280
288
|
required: z.boolean().default(true),
|
|
281
289
|
/** Checked against the connected store's accessLevel at install. */
|
|
@@ -2040,7 +2048,11 @@ var composeInputsSchema = z7.strictObject({
|
|
|
2040
2048
|
// A row with an empty binding key must not fail a deploy: the rail
|
|
2041
2049
|
// treats falsy as "no binding key", so normalise rather than reject.
|
|
2042
2050
|
bindingKey: z7.string().nullish().transform((value) => value || null),
|
|
2043
|
-
accessLevel: z7.enum(["read", "write", "admin"])
|
|
2051
|
+
accessLevel: z7.enum(["read", "write", "admin"]),
|
|
2052
|
+
// Defaulted so a file written before the platform emitted store types
|
|
2053
|
+
// (database rows only) still parses. The platform starts writing
|
|
2054
|
+
// storage rows, with `type` set, once forks run a compose that reads it.
|
|
2055
|
+
type: z7.enum(["database", "storage"]).default("database")
|
|
2044
2056
|
})
|
|
2045
2057
|
).default([]),
|
|
2046
2058
|
installs: z7.record(
|
|
@@ -2051,6 +2063,17 @@ var composeInputsSchema = z7.strictObject({
|
|
|
2051
2063
|
})
|
|
2052
2064
|
).default({})
|
|
2053
2065
|
});
|
|
2066
|
+
var STUB_IGNORE_PATH = "apps/web/src/app/.gitignore";
|
|
2067
|
+
var STUB_IGNORE_HEADER = "# Generated by stardeck-compose \u2014 Module-rail stubs are build outputs; do not edit or commit them.";
|
|
2068
|
+
function escapeStubIgnorePattern(pattern) {
|
|
2069
|
+
const escaped = pattern.replace(/[\\[\]*?]/g, (char) => `\\${char}`);
|
|
2070
|
+
return /^[#!]/.test(escaped) ? `\\${escaped}` : escaped;
|
|
2071
|
+
}
|
|
2072
|
+
function buildStubIgnore(artifactPaths) {
|
|
2073
|
+
const prefix = "apps/web/src/app/";
|
|
2074
|
+
const entries = artifactPaths.filter((artifact) => artifact.startsWith(prefix)).map((artifact) => escapeStubIgnorePattern(`/${artifact.slice(prefix.length)}`)).sort();
|
|
2075
|
+
return [STUB_IGNORE_HEADER, ...entries].join("\n") + "\n";
|
|
2076
|
+
}
|
|
2054
2077
|
|
|
2055
2078
|
// ../../packages/lib/src/shared/module-enablement.ts
|
|
2056
2079
|
function isModuleEnabledByList(raw, name, kindOf) {
|
|
@@ -3556,7 +3579,9 @@ function dirnamePosix(filePath) {
|
|
|
3556
3579
|
// ../../packages/lib/src/server/module-rail/compose-inputs.ts
|
|
3557
3580
|
async function buildComposeInputs(input) {
|
|
3558
3581
|
const { db, projectId, deps } = input;
|
|
3559
|
-
const connectedStores = await deps.
|
|
3582
|
+
const connectedStores = (await deps.listConnectedStoresForProject(db, projectId)).filter(
|
|
3583
|
+
(store) => store.type === "database"
|
|
3584
|
+
);
|
|
3560
3585
|
const installRows = await Promise.all(
|
|
3561
3586
|
input.moduleNames.map(async (moduleName) => ({
|
|
3562
3587
|
moduleName,
|
|
@@ -3571,7 +3596,11 @@ async function buildComposeInputs(input) {
|
|
|
3571
3596
|
datastoreBindings: install.datastoreBindings ?? null
|
|
3572
3597
|
};
|
|
3573
3598
|
}
|
|
3574
|
-
|
|
3599
|
+
const parsed = composeInputsSchema.parse({ connectedStores, installs });
|
|
3600
|
+
return {
|
|
3601
|
+
...parsed,
|
|
3602
|
+
connectedStores: parsed.connectedStores.map(({ type: _type, ...store }) => store)
|
|
3603
|
+
};
|
|
3575
3604
|
}
|
|
3576
3605
|
|
|
3577
3606
|
// ../../packages/lib/src/shared/data-store-manifest.ts
|
|
@@ -3597,7 +3626,7 @@ function compareStores(a, b) {
|
|
|
3597
3626
|
function describeCandidates(stores) {
|
|
3598
3627
|
if (stores.length === 0) return "none";
|
|
3599
3628
|
return stores.map(
|
|
3600
|
-
(store) => `${store.storeId} (slug=${store.slug}, bindingKey=${store.bindingKey ?? "none"}, access=${store.accessLevel})`
|
|
3629
|
+
(store) => `${store.storeId} (type=${store.type}, slug=${store.slug}, bindingKey=${store.bindingKey ?? "none"}, access=${store.accessLevel})`
|
|
3601
3630
|
).join(", ");
|
|
3602
3631
|
}
|
|
3603
3632
|
function groupStoresById(stores) {
|
|
@@ -3628,14 +3657,15 @@ function accessLevelForBinding(group, binding) {
|
|
|
3628
3657
|
);
|
|
3629
3658
|
}
|
|
3630
3659
|
function resolveModuleDataStoreBindings(input) {
|
|
3631
|
-
const
|
|
3632
|
-
const storeGroups = groupStoresById(stores);
|
|
3660
|
+
const allStores = [...input.connectedStores].sort(compareStores);
|
|
3633
3661
|
const requirements = [...input.requirements].sort((a, b) => compareStrings(a.id, b.id));
|
|
3634
3662
|
const overrides = input.explicitOverrides ?? {};
|
|
3635
3663
|
const bindings = {};
|
|
3636
3664
|
const errors = [];
|
|
3637
|
-
const candidates = describeCandidates(stores);
|
|
3638
3665
|
for (const requirement of requirements) {
|
|
3666
|
+
const stores = allStores.filter((store) => store.type === requirement.type);
|
|
3667
|
+
const storeGroups = groupStoresById(stores);
|
|
3668
|
+
const candidates = describeCandidates(allStores);
|
|
3639
3669
|
const logicalSlug = dataStoreSlug(requirement.id);
|
|
3640
3670
|
let selected;
|
|
3641
3671
|
if (Object.prototype.hasOwnProperty.call(overrides, requirement.id)) {
|
|
@@ -3648,7 +3678,7 @@ function resolveModuleDataStoreBindings(input) {
|
|
|
3648
3678
|
moduleName: input.moduleName,
|
|
3649
3679
|
logicalId: requirement.id,
|
|
3650
3680
|
kind: "explicit-override",
|
|
3651
|
-
message: matches.length === 0 ? `Module "${input.moduleName}" data store "${requirement.id}" override "${override}" did not match a connected
|
|
3681
|
+
message: matches.length === 0 ? `Module "${input.moduleName}" data store "${requirement.id}" override "${override}" did not match a connected ${requirement.type} store; candidates: ${candidates}` : `Module "${input.moduleName}" data store "${requirement.id}" override "${override}" is ambiguous; matches: ${describeCandidates(matches.flatMap((group) => group.stores))}`
|
|
3652
3682
|
});
|
|
3653
3683
|
continue;
|
|
3654
3684
|
}
|
|
@@ -3659,6 +3689,8 @@ function resolveModuleDataStoreBindings(input) {
|
|
|
3659
3689
|
);
|
|
3660
3690
|
if (conventionMatches.length === 1) {
|
|
3661
3691
|
selected = conventionMatches[0];
|
|
3692
|
+
} else if (conventionMatches.length === 0 && requirement.type === "storage") {
|
|
3693
|
+
if (storeGroups.length === 1) selected = storeGroups[0];
|
|
3662
3694
|
} else if (conventionMatches.length === 0 && requirement.scope === "app") {
|
|
3663
3695
|
if (storeGroups.length === 1) {
|
|
3664
3696
|
selected = storeGroups[0];
|
|
@@ -4017,7 +4049,7 @@ async function resolveCompositionDataStoreBindings(context, presentNames, manife
|
|
|
4017
4049
|
if (modulesWithRequirements.length === 0) return bindingsByModule;
|
|
4018
4050
|
const deps = context.deps;
|
|
4019
4051
|
if (!deps) throw new Error("Module rail dependencies are required for data-store bindings");
|
|
4020
|
-
const connectedStores = await deps.
|
|
4052
|
+
const connectedStores = await deps.listConnectedStoresForProject(context.db, context.projectId);
|
|
4021
4053
|
const installs = await Promise.all(
|
|
4022
4054
|
modulesWithRequirements.map(
|
|
4023
4055
|
(moduleName) => deps.getModuleInstall(context.db, context.projectId, moduleName)
|
|
@@ -4307,6 +4339,21 @@ async function writeComposeInputs(target) {
|
|
|
4307
4339
|
);
|
|
4308
4340
|
return written;
|
|
4309
4341
|
}
|
|
4342
|
+
async function readStubIgnore(context) {
|
|
4343
|
+
const existing = await context.sandbox.fileExists(STUB_IGNORE_PATH) ? await context.sandbox.readFile(STUB_IGNORE_PATH) : null;
|
|
4344
|
+
if (existing !== null && existing.split("\n", 1)[0] !== STUB_IGNORE_HEADER) {
|
|
4345
|
+
throw new Error(
|
|
4346
|
+
`[ModuleRail] refusing to overwrite ${STUB_IGNORE_PATH}: it is not compose-generated (line 1 is not the generated header)`
|
|
4347
|
+
);
|
|
4348
|
+
}
|
|
4349
|
+
return existing;
|
|
4350
|
+
}
|
|
4351
|
+
async function writeStubIgnore(context, existing, artifactPaths) {
|
|
4352
|
+
const content = buildStubIgnore(artifactPaths);
|
|
4353
|
+
const written = existing === null || existing.trimEnd() !== content.trimEnd();
|
|
4354
|
+
if (written) await writeSandboxFileAtomic(context, STUB_IGNORE_PATH, content);
|
|
4355
|
+
return written;
|
|
4356
|
+
}
|
|
4310
4357
|
async function applyCompositionArtifactMutations(context, plan, existingFiles) {
|
|
4311
4358
|
const appliedWrites = [];
|
|
4312
4359
|
const appliedDeletes = [];
|
|
@@ -5171,9 +5218,17 @@ async function reconcileCompositionArtifacts(context, options = {}) {
|
|
|
5171
5218
|
deps
|
|
5172
5219
|
});
|
|
5173
5220
|
}
|
|
5221
|
+
const maintainIgnore = !options.dryRun && !options.enabledModulesRaw;
|
|
5222
|
+
const existingIgnore = maintainIgnore ? await readStubIgnore(context) : null;
|
|
5174
5223
|
if (changed && !options.dryRun) {
|
|
5175
5224
|
await applyCompositionArtifactMutations(context, planned.plan, existingFiles);
|
|
5176
5225
|
}
|
|
5226
|
+
if (maintainIgnore) {
|
|
5227
|
+
const wroteIgnore = await writeStubIgnore(context, existingIgnore, planned.plan.desiredPaths);
|
|
5228
|
+
console.log(
|
|
5229
|
+
`[ModuleRail] runId=${context.runId} stub ignore list entries=${planned.plan.desiredPaths.length} written=${wroteIgnore}`
|
|
5230
|
+
);
|
|
5231
|
+
}
|
|
5177
5232
|
return {
|
|
5178
5233
|
registryPath: planned.plan.registryPath,
|
|
5179
5234
|
writePaths: planned.plan.writes.map((w) => w.path),
|
|
@@ -5210,7 +5265,8 @@ function readComposeInputs(cwd) {
|
|
|
5210
5265
|
storeId: store.storeId,
|
|
5211
5266
|
slug: store.slug,
|
|
5212
5267
|
bindingKey: store.bindingKey ?? null,
|
|
5213
|
-
accessLevel: store.accessLevel
|
|
5268
|
+
accessLevel: store.accessLevel,
|
|
5269
|
+
type: store.type
|
|
5214
5270
|
})),
|
|
5215
5271
|
installs: parsed.data.installs
|
|
5216
5272
|
};
|
|
@@ -5344,7 +5400,7 @@ async function compose(options) {
|
|
|
5344
5400
|
const deps = {
|
|
5345
5401
|
sandbox,
|
|
5346
5402
|
getModuleInstall: async (_db, _projectId, moduleName) => inputs.installs[moduleName] ?? null,
|
|
5347
|
-
|
|
5403
|
+
listConnectedStoresForProject: async () => inputs.connectedStores
|
|
5348
5404
|
};
|
|
5349
5405
|
const context = {
|
|
5350
5406
|
db: {},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stardeck-customer-apps/compose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Regenerates a Stardeck app's Module-rail artifacts — the five src/*.gen.ts registries and every route/endpoint stub — from src/modules/*/module.json",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|