Skip to content

Commit dbb39ce

Browse files
authored
Merge pull request #338 from weaviate/feat/overwriteAlias
feat: `overwriteAlias` for backup restore
2 parents 1ac41ec + aed2a59 commit dbb39ce

File tree

6 files changed

+240
-5
lines changed

6 files changed

+240
-5
lines changed

src/backup/backupRestorer.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export default class BackupRestorer extends CommandBase {
2626
private statusGetter: BackupRestoreStatusGetter;
2727
private waitForCompletion?: boolean;
2828
private config?: RestoreConfig;
29+
private overwriteAlias?: boolean;
2930

3031
constructor(client: Connection, statusGetter: BackupRestoreStatusGetter) {
3132
super(client);
@@ -65,6 +66,11 @@ export default class BackupRestorer extends CommandBase {
6566
return this;
6667
}
6768

69+
withOverwriteAlias(overwriteAlias: boolean) {
70+
this.overwriteAlias = overwriteAlias;
71+
return this;
72+
}
73+
6874
withConfig(cfg: RestoreConfig) {
6975
this.config = cfg;
7076
return this;
@@ -89,6 +95,7 @@ export default class BackupRestorer extends CommandBase {
8995
config: this.config,
9096
include: this.includeClassNames,
9197
exclude: this.excludeClassNames,
98+
overwriteAlias: this.overwriteAlias,
9299
} as BackupRestoreRequest;
93100

94101
if (this.waitForCompletion) {

src/collections/backup/client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,9 @@ export const backup = (connection: Connection): Backup => {
163163
if (args.excludeCollections) {
164164
builder = builder.withExcludeClassNames(...args.excludeCollections);
165165
}
166+
if (args.config?.overwriteAlias) {
167+
builder = builder.withOverwriteAlias(args.config?.overwriteAlias);
168+
}
166169
if (args.config) {
167170
builder = builder.withConfig({
168171
CPUPercentage: args.config.cpuPercentage,

src/collections/backup/collection.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Backend } from '../../backup/index.js';
22
import Connection from '../../connection/index.js';
3-
import { WeaviateInvalidInputError } from '../../errors.js';
43
import { backup } from './client.js';
54
import { BackupReturn, BackupStatusArgs, BackupStatusReturn } from './types.js';
65

src/collections/backup/integration.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
/* eslint-disable no-await-in-loop */
44
import { requireAtLeast } from '../../../test/version.js';
55
import { Backend } from '../../backup/index.js';
6+
import { WeaviateBackupFailed } from '../../errors.js';
67
import weaviate, { Collection, WeaviateClient } from '../../index.js';
78

89
// These must run sequentially because Weaviate is not capable of running multiple backups at the same time
@@ -117,6 +118,87 @@ describe('Integration testing of backups', () => {
117118
.then(testCollectionWaitForCompletion)
118119
.then(testCollectionNoWaitForCompletion));
119120

121+
requireAtLeast(1, 32, 3).describe('overwrite alias', () => {
122+
test('overwriteAlias=true', async () => {
123+
const client = await clientPromise;
124+
125+
const things = await client.collections.create({ name: 'ThingsTrue' });
126+
await client.alias.create({ collection: things.name, alias: `${things.name}Alias` });
127+
128+
const backup = await client.backup.create({
129+
backend: 'filesystem',
130+
backupId: randomBackupId(),
131+
includeCollections: [things.name],
132+
waitForCompletion: true,
133+
});
134+
135+
await client.collections.delete(things.name);
136+
await client.alias.delete(`${things.name}Alias`);
137+
138+
// Change alias to point to a different collection
139+
const inventory = await client.collections.create({ name: 'InventoryTrue' });
140+
await client.alias.create({ collection: inventory.name, alias: `${things.name}Alias` });
141+
142+
// Restore backup with overwriteAlias=true
143+
await client.backup.restore({
144+
backend: 'filesystem',
145+
backupId: backup.id,
146+
includeCollections: [things.name],
147+
waitForCompletion: true,
148+
config: { overwriteAlias: true },
149+
});
150+
151+
// Assert: alias points to the original collection
152+
const alias = await client.alias.get(`${things.name}Alias`);
153+
expect(alias.collection).toEqual(things.name);
154+
});
155+
156+
test('overwriteAlias=false', async () => {
157+
const client = await clientPromise;
158+
159+
const things = await client.collections.create({ name: 'ThingsFalse' });
160+
await client.alias.create({ collection: things.name, alias: `${things.name}Alias` });
161+
162+
const backup = await client.backup.create({
163+
backend: 'filesystem',
164+
backupId: randomBackupId(),
165+
includeCollections: [things.name],
166+
waitForCompletion: true,
167+
});
168+
169+
await client.collections.delete(things.name);
170+
await client.alias.delete(`${things.name}Alias`);
171+
172+
// Change alias to point to a different collection
173+
const inventory = await client.collections.create({ name: 'InventoryFalse' });
174+
await client.alias.create({ collection: inventory.name, alias: `${things.name}Alias` });
175+
176+
// Restore backup with overwriteAlias=true
177+
const restored = client.backup.restore({
178+
backend: 'filesystem',
179+
backupId: backup.id,
180+
includeCollections: [things.name],
181+
waitForCompletion: true,
182+
config: { overwriteAlias: false },
183+
});
184+
185+
// Assert: fails with "alias already exists" error
186+
await expect(restored).rejects.toThrowError(WeaviateBackupFailed);
187+
});
188+
189+
it('cleanup', async () => {
190+
await clientPromise.then(async (c) => {
191+
await Promise.all(
192+
['ThingsTrue', 'ThingsFalse', 'InventoryTrue', 'InventoryFalse'].map((name) =>
193+
c.collections.delete(name).catch((e) => {})
194+
)
195+
);
196+
await c.alias.delete('ThingsFalseAlias').catch((e) => {});
197+
await c.alias.delete('ThingsTrueAlias').catch((e) => {});
198+
});
199+
});
200+
});
201+
120202
requireAtLeast(1, 32, 0).it('get all exising backups', async () => {
121203
await clientPromise.then(async (client) => {
122204
await client.collections.create({ name: 'TestListBackups' }).then((col) => col.data.insert());

src/collections/backup/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ export type BackupConfigCreate = {
3737
export type BackupConfigRestore = {
3838
/** The percentage of CPU to use for the backuop restoration job. */
3939
cpuPercentage?: number;
40+
/** Allows ovewriting the collection alias if there is a conflict. */
41+
overwriteAlias?: boolean;
4042
};
4143

4244
/** The arguments required to create and restore backups. */

src/openapi/schema.ts

Lines changed: 146 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ export interface paths {
110110
'/authz/roles/{id}/user-assignments': {
111111
get: operations['getUsersForRole'];
112112
};
113+
'/authz/roles/{id}/group-assignments': {
114+
/** Retrieves a list of all groups that have been assigned a specific role, identified by its name. */
115+
get: operations['getGroupsForRole'];
116+
};
113117
'/authz/users/{id}/roles': {
114118
get: operations['getRolesForUserDeprecated'];
115119
};
@@ -128,6 +132,14 @@ export interface paths {
128132
'/authz/groups/{id}/revoke': {
129133
post: operations['revokeRoleFromGroup'];
130134
};
135+
'/authz/groups/{id}/roles/{groupType}': {
136+
/** Retrieves a list of all roles assigned to a specific group. The group must be identified by both its name (`id`) and its type (`db` or `oidc`). */
137+
get: operations['getRolesForGroup'];
138+
};
139+
'/authz/groups/{groupType}': {
140+
/** Retrieves a list of all available group names for a specified group type (`oidc` or `db`). */
141+
get: operations['getGroups'];
142+
};
131143
'/objects': {
132144
/** Lists all Objects in reverse order of creation, owned by the user that belongs to the used token. */
133145
get: operations['objects.list'];
@@ -303,6 +315,11 @@ export interface definitions {
303315
* @enum {string}
304316
*/
305317
UserTypeInput: 'db' | 'oidc';
318+
/**
319+
* @description If the group contains OIDC or database users.
320+
* @enum {string}
321+
*/
322+
GroupType: 'db' | 'oidc';
306323
/**
307324
* @description the type of user
308325
* @enum {string}
@@ -399,6 +416,15 @@ export interface definitions {
399416
*/
400417
users?: string;
401418
};
419+
/** @description Resources applicable for group actions. */
420+
groups?: {
421+
/**
422+
* @description A string that specifies which groups this permission applies to. Can be an exact group name or a regex pattern. The default value `*` applies the permission to all groups.
423+
* @default *
424+
*/
425+
group?: string;
426+
groupType?: definitions['GroupType'];
427+
};
402428
/** @description resources applicable for tenant actions */
403429
tenants?: {
404430
/**
@@ -496,7 +522,9 @@ export interface definitions {
496522
| 'create_aliases'
497523
| 'read_aliases'
498524
| 'update_aliases'
499-
| 'delete_aliases';
525+
| 'delete_aliases'
526+
| 'assign_and_revoke_groups'
527+
| 'read_groups';
500528
};
501529
/** @description list of roles */
502530
RolesListResponse: definitions['Role'][];
@@ -1171,8 +1199,6 @@ export interface definitions {
11711199
BackupListResponse: {
11721200
/** @description The ID of the backup. Must be URL-safe and work as a filesystem path, only lowercase, numbers, underscore, minus characters allowed. */
11731201
id?: string;
1174-
/** @description destination path of backup files proper to selected backend */
1175-
path?: string;
11761202
/** @description The list of classes for which the existed backup process */
11771203
classes?: string[];
11781204
/**
@@ -1191,6 +1217,8 @@ export interface definitions {
11911217
exclude?: string[];
11921218
/** @description Allows overriding the node names stored in the backup with different ones. Useful when restoring backups to a different environment. */
11931219
node_mapping?: { [key: string]: string };
1220+
/** @description Allows ovewriting the collection alias if there is a conflict */
1221+
overwriteAlias?: boolean;
11941222
};
11951223
/** @description The definition of a backup restore response body */
11961224
BackupRestoreResponse: {
@@ -1789,7 +1817,9 @@ export interface definitions {
17891817
| 'WithinGeoRange'
17901818
| 'IsNull'
17911819
| 'ContainsAny'
1792-
| 'ContainsAll';
1820+
| 'ContainsAll'
1821+
| 'ContainsNone'
1822+
| 'Not';
17931823
/**
17941824
* @description path to the property currently being filtered
17951825
* @example [
@@ -2827,6 +2857,42 @@ export interface operations {
28272857
};
28282858
};
28292859
};
2860+
/** Retrieves a list of all groups that have been assigned a specific role, identified by its name. */
2861+
getGroupsForRole: {
2862+
parameters: {
2863+
path: {
2864+
/** The unique name of the role. */
2865+
id: string;
2866+
};
2867+
};
2868+
responses: {
2869+
/** Successfully retrieved the list of groups that have the role assigned. */
2870+
200: {
2871+
schema: ({
2872+
groupId?: string;
2873+
groupType: definitions['GroupType'];
2874+
} & {
2875+
name: unknown;
2876+
})[];
2877+
};
2878+
/** Bad request */
2879+
400: {
2880+
schema: definitions['ErrorResponse'];
2881+
};
2882+
/** Unauthorized or invalid credentials. */
2883+
401: unknown;
2884+
/** Forbidden */
2885+
403: {
2886+
schema: definitions['ErrorResponse'];
2887+
};
2888+
/** The specified role was not found. */
2889+
404: unknown;
2890+
/** An error has occurred while trying to fulfill the request. Most likely the ErrorResponse will contain more information about the error. */
2891+
500: {
2892+
schema: definitions['ErrorResponse'];
2893+
};
2894+
};
2895+
};
28302896
getRolesForUserDeprecated: {
28312897
parameters: {
28322898
path: {
@@ -2985,6 +3051,7 @@ export interface operations {
29853051
body: {
29863052
/** @description the roles that assigned to group */
29873053
roles?: string[];
3054+
groupType?: definitions['GroupType'];
29883055
};
29893056
};
29903057
};
@@ -3019,6 +3086,7 @@ export interface operations {
30193086
body: {
30203087
/** @description the roles that revoked from group */
30213088
roles?: string[];
3089+
groupType?: definitions['GroupType'];
30223090
};
30233091
};
30243092
};
@@ -3043,6 +3111,80 @@ export interface operations {
30433111
};
30443112
};
30453113
};
3114+
/** Retrieves a list of all roles assigned to a specific group. The group must be identified by both its name (`id`) and its type (`db` or `oidc`). */
3115+
getRolesForGroup: {
3116+
parameters: {
3117+
path: {
3118+
/** The unique name of the group. */
3119+
id: string;
3120+
/** The type of the group. */
3121+
groupType: 'oidc';
3122+
};
3123+
query: {
3124+
/** If true, the response will include the full role definitions with all associated permissions. If false, only role names are returned. */
3125+
includeFullRoles?: boolean;
3126+
};
3127+
};
3128+
responses: {
3129+
/** A list of roles assigned to the specified group. */
3130+
200: {
3131+
schema: definitions['RolesListResponse'];
3132+
};
3133+
/** Bad request */
3134+
400: {
3135+
schema: definitions['ErrorResponse'];
3136+
};
3137+
/** Unauthorized or invalid credentials. */
3138+
401: unknown;
3139+
/** Forbidden */
3140+
403: {
3141+
schema: definitions['ErrorResponse'];
3142+
};
3143+
/** The specified group was not found. */
3144+
404: unknown;
3145+
/** The request syntax is correct, but the server couldn't process it due to semantic issues. */
3146+
422: {
3147+
schema: definitions['ErrorResponse'];
3148+
};
3149+
/** An error has occurred while trying to fulfill the request. Most likely the ErrorResponse will contain more information about the error. */
3150+
500: {
3151+
schema: definitions['ErrorResponse'];
3152+
};
3153+
};
3154+
};
3155+
/** Retrieves a list of all available group names for a specified group type (`oidc` or `db`). */
3156+
getGroups: {
3157+
parameters: {
3158+
path: {
3159+
/** The type of group to retrieve. */
3160+
groupType: 'oidc';
3161+
};
3162+
};
3163+
responses: {
3164+
/** A list of group names for the specified type. */
3165+
200: {
3166+
schema: string[];
3167+
};
3168+
/** Bad request */
3169+
400: {
3170+
schema: definitions['ErrorResponse'];
3171+
};
3172+
/** Unauthorized or invalid credentials. */
3173+
401: unknown;
3174+
/** Forbidden */
3175+
403: {
3176+
schema: definitions['ErrorResponse'];
3177+
};
3178+
/** The request syntax is correct, but the server couldn't process it due to semantic issues. */
3179+
422: {
3180+
schema: definitions['ErrorResponse'];
3181+
};
3182+
/** An error has occurred while trying to fulfill the request. Most likely the ErrorResponse will contain more information about the error. */
3183+
500: {
3184+
schema: definitions['ErrorResponse'];
3185+
};
3186+
};
3187+
};
30463188
/** Lists all Objects in reverse order of creation, owned by the user that belongs to the used token. */
30473189
'objects.list': {
30483190
parameters: {

0 commit comments

Comments
 (0)