import {
|
listCustomerNumbers as listCustomerNumbersInternal,
|
readCatalog as readCatalogInternal,
|
selectableFor,
|
} from "@dh-software/baukasten-permissions";
|
import {
|
readConfiguration as readConfigurationInternal,
|
readConfigurations as readConfigurationsInternal,
|
resolveConfigurationPath as resolveConfigurationPathInternal,
|
} from "@dh-software/baukasten-config-reader";
|
import type {
|
ConfigurationEntry,
|
ConfigurationReference,
|
CustomerNumber,
|
JsonValue,
|
StoreMembershipResolver,
|
} from "@dh-software/baukasten-types";
|
|
/** Options for {@link createBaukasten}. */
|
export interface BaukastenOptions {
|
/** Absolute path to the baukasten "configurations" directory. */
|
configurationsDir: string;
|
/**
|
* Resolves whether a requesting customer is part of an owner's StoreTree
|
* (furncloudcredentials). Supplied by the host (furnplan_web) so the library
|
* never touches the database. May be sync or async. When omitted, "stores"
|
* grants never match foreign customers.
|
*/
|
isStoreMember?: StoreMembershipResolver;
|
}
|
|
/**
|
* The stable baukasten API surface. Consumers (furnplan_web) depend only on this
|
* contract; the internal package layout and helper signatures may change freely
|
* as long as these method shapes stay the same.
|
*/
|
export interface Baukasten {
|
/** All customer-number folders present under the configurations directory. */
|
listCustomerNumbers(): Promise<CustomerNumber[]>;
|
/** The full catalog across all customers (display-names + resolved permissions). */
|
getCatalog(): Promise<ConfigurationEntry[]>;
|
/** The catalog filtered to what `requestingCustomerNumber` may select (for the AUC). */
|
getSelectableConfigurations(
|
requestingCustomerNumber: CustomerNumber,
|
): Promise<ConfigurationEntry[]>;
|
/** Resolves a reference to its absolute file path (throws if missing). */
|
resolveConfigurationPath(reference: ConfigurationReference): Promise<string>;
|
/** Reads & parses a single referenced configuration. */
|
readConfiguration(reference: ConfigurationReference): Promise<JsonValue>;
|
/** Reads & parses an ordered list of references, preserving merge order. */
|
readConfigurations(references: ConfigurationReference[]): Promise<JsonValue[]>;
|
}
|
|
/** Creates a {@link Baukasten} bound to one configurations directory + store resolver. */
|
export function createBaukasten(options: BaukastenOptions): Baukasten {
|
const { configurationsDir, isStoreMember } = options;
|
|
return {
|
listCustomerNumbers() {
|
return listCustomerNumbersInternal(configurationsDir);
|
},
|
getCatalog() {
|
return readCatalogInternal(configurationsDir);
|
},
|
async getSelectableConfigurations(requestingCustomerNumber) {
|
const catalog = await readCatalogInternal(configurationsDir);
|
|
// Pre-resolve the (possibly async) StoreTree membership only for the
|
// owners whose visibility actually depends on it, then filter synchronously.
|
const storeOwners = new Set(
|
catalog
|
.filter(
|
(entry) =>
|
entry.permission.blanket === "stores" &&
|
entry.permission.owner !== requestingCustomerNumber &&
|
!entry.permission.consumers.includes(requestingCustomerNumber),
|
)
|
.map((entry) => entry.permission.owner),
|
);
|
|
const memberOf = new Set<CustomerNumber>();
|
for (const owner of storeOwners) {
|
if (isStoreMember && (await isStoreMember(owner, requestingCustomerNumber))) {
|
memberOf.add(owner);
|
}
|
}
|
|
return selectableFor(catalog, requestingCustomerNumber, {
|
isStoreMember: (owner) => memberOf.has(owner),
|
});
|
},
|
resolveConfigurationPath(reference) {
|
return resolveConfigurationPathInternal(configurationsDir, reference);
|
},
|
readConfiguration(reference) {
|
return readConfigurationInternal(configurationsDir, reference);
|
},
|
readConfigurations(references) {
|
return readConfigurationsInternal(configurationsDir, references);
|
},
|
};
|
}
|