Skip to content

Commit 4743aba

Browse files
committed
feat(deploy): new function buildCouchbaseClusterConfig
Use to meet the target config regardless of the current state of the cluster
1 parent 09ffd13 commit 4743aba

3 files changed

Lines changed: 425 additions & 0 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import {
2+
AnyCluster,
3+
BucketSettings,
4+
CollectionSpec,
5+
ISearchIndex,
6+
SearchIndex,
7+
ScopeSpec,
8+
} from '@cbjsdev/cbjs';
9+
10+
import {
11+
CouchbaseClusterBucketConfig,
12+
CouchbaseClusterCollectionConfig,
13+
CouchbaseClusterCollectionIndexConfig,
14+
CouchbaseClusterConfig,
15+
CouchbaseClusterScopeConfig,
16+
CouchbaseClusterSearchIndexConfig,
17+
} from './types.js';
18+
19+
export type BuildCouchbaseClusterConfigOptions = {
20+
/**
21+
* Only include the specified buckets. All buckets are included when omitted.
22+
*/
23+
buckets?: string[];
24+
};
25+
26+
/**
27+
* Build a {@link CouchbaseClusterConfig} by querying the live cluster.
28+
*
29+
* The returned config can be passed as the `currentConfig` argument
30+
* to {@link getCouchbaseClusterChanges} to diff against a desired state.
31+
*
32+
* @remarks
33+
* - Search indexes are keyed by their actual index name (the cluster has no concept of config aliases).
34+
* - User passwords are never returned by the server; password-related diff changes will always
35+
* be emitted when the target config specifies a password.
36+
* - Query index `numReplicas` is not available through the SDK and is omitted from the config.
37+
*/
38+
export async function buildCouchbaseClusterConfig(
39+
cluster: AnyCluster,
40+
options?: BuildCouchbaseClusterConfigOptions
41+
): Promise<CouchbaseClusterConfig> {
42+
const allBuckets = await cluster.buckets().getAllBuckets();
43+
const buckets = options?.buckets
44+
? allBuckets.filter((b) => options.buckets!.includes(b.name))
45+
: allBuckets;
46+
47+
const [keyspaceEntries, users] = await Promise.all([
48+
Promise.all(buckets.map((b) => buildBucketEntry(cluster, b))),
49+
cluster.users().getAllUsers(),
50+
]);
51+
52+
return {
53+
keyspaces: Object.fromEntries(keyspaceEntries),
54+
users: users.map(({ username, displayName, groups, roles, domain }) => ({
55+
username,
56+
displayName,
57+
groups,
58+
roles,
59+
domain,
60+
})),
61+
};
62+
}
63+
64+
async function buildBucketEntry(
65+
cluster: AnyCluster,
66+
bucket: BucketSettings
67+
): Promise<[string, CouchbaseClusterBucketConfig]> {
68+
const { name, ...settings } = bucket;
69+
const scopeSpecs = await cluster.bucket(name).collections().getAllScopes();
70+
71+
const scopeEntries = await Promise.all(
72+
scopeSpecs.map((scope) => buildScopeEntry(cluster, name, scope))
73+
);
74+
75+
return [name, { ...settings, scopes: Object.fromEntries(scopeEntries) }];
76+
}
77+
78+
async function buildScopeEntry(
79+
cluster: AnyCluster,
80+
bucketName: string,
81+
scope: ScopeSpec
82+
): Promise<[string, CouchbaseClusterScopeConfig]> {
83+
const [collectionEntries, searchIndexes] = await Promise.all([
84+
Promise.all(
85+
scope.collections.map((col) =>
86+
buildCollectionEntry(cluster, bucketName, scope.name, col)
87+
)
88+
),
89+
cluster.bucket(bucketName).scope(scope.name).searchIndexes().getAllIndexes(),
90+
]);
91+
92+
const config: CouchbaseClusterScopeConfig = {
93+
collections: Object.fromEntries(collectionEntries),
94+
};
95+
96+
if (searchIndexes.length > 0) {
97+
config.searchIndexes = Object.fromEntries(
98+
searchIndexes.map((idx) => [idx.name, toSearchIndexConfigFn(idx)])
99+
);
100+
}
101+
102+
return [scope.name, config];
103+
}
104+
105+
async function buildCollectionEntry(
106+
cluster: AnyCluster,
107+
bucketName: string,
108+
scopeName: string,
109+
col: CollectionSpec
110+
): Promise<[string, CouchbaseClusterCollectionConfig]> {
111+
const queryIndexes = await cluster
112+
.bucket(bucketName)
113+
.scope(scopeName)
114+
.collection(col.name)
115+
.queryIndexes()
116+
.getAllIndexes();
117+
118+
const config: CouchbaseClusterCollectionConfig = {
119+
maxExpiry: col.maxExpiry || undefined,
120+
history: col.history,
121+
};
122+
123+
const secondaryIndexes = queryIndexes.filter((qi) => !qi.isPrimary);
124+
if (secondaryIndexes.length > 0) {
125+
config.indexes = Object.fromEntries(
126+
secondaryIndexes.map((qi) => [qi.name, toQueryIndexConfig(qi)])
127+
);
128+
}
129+
130+
return [col.name, config];
131+
}
132+
133+
function toQueryIndexConfig(
134+
qi: { indexKey: string[]; condition?: string }
135+
): CouchbaseClusterCollectionIndexConfig {
136+
const config: CouchbaseClusterCollectionIndexConfig = {
137+
keys: qi.indexKey,
138+
};
139+
140+
if (qi.condition) {
141+
config.where = qi.condition;
142+
}
143+
144+
return config;
145+
}
146+
147+
/**
148+
* Wrap a live {@link SearchIndex} into a config function.
149+
* Server-assigned fields (`uuid`, `sourceUUID`) are stripped to avoid phantom diffs
150+
* during comparison, since they change on every index mutation.
151+
*/
152+
function toSearchIndexConfigFn(
153+
searchIndex: SearchIndex
154+
): CouchbaseClusterSearchIndexConfig {
155+
const config = {
156+
name: searchIndex.name,
157+
sourceName: searchIndex.sourceName,
158+
type: searchIndex.type,
159+
params: searchIndex.params,
160+
sourceType: searchIndex.sourceType,
161+
sourceParams: searchIndex.sourceParams,
162+
planParams: searchIndex.planParams,
163+
} as ISearchIndex;
164+
165+
return () => config;
166+
}

packages/deploy/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './clusterChanges/types.js';
2+
export * from './clusterChanges/buildCouchbaseClusterConfig.js';
23
export * from './clusterChanges/getCouchbaseClusterChanges.js';
34
export * from './clusterChanges/applyCouchbaseClusterChanges.js';

0 commit comments

Comments
 (0)