From bff4dd3e54d1b578135dc4f946028e49a0900317 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Thu, 28 Aug 2025 11:20:46 +0200 Subject: [PATCH 1/4] chore: refresh OpenAPI schema --- src/openapi/schema.ts | 150 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 146 insertions(+), 4 deletions(-) diff --git a/src/openapi/schema.ts b/src/openapi/schema.ts index 9908f40d..409fbfbe 100644 --- a/src/openapi/schema.ts +++ b/src/openapi/schema.ts @@ -110,6 +110,10 @@ export interface paths { '/authz/roles/{id}/user-assignments': { get: operations['getUsersForRole']; }; + '/authz/roles/{id}/group-assignments': { + /** Retrieves a list of all groups that have been assigned a specific role, identified by its name. */ + get: operations['getGroupsForRole']; + }; '/authz/users/{id}/roles': { get: operations['getRolesForUserDeprecated']; }; @@ -128,6 +132,14 @@ export interface paths { '/authz/groups/{id}/revoke': { post: operations['revokeRoleFromGroup']; }; + '/authz/groups/{id}/roles/{groupType}': { + /** 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`). */ + get: operations['getRolesForGroup']; + }; + '/authz/groups/{groupType}': { + /** Retrieves a list of all available group names for a specified group type (`oidc` or `db`). */ + get: operations['getGroups']; + }; '/objects': { /** Lists all Objects in reverse order of creation, owned by the user that belongs to the used token. */ get: operations['objects.list']; @@ -303,6 +315,11 @@ export interface definitions { * @enum {string} */ UserTypeInput: 'db' | 'oidc'; + /** + * @description If the group contains OIDC or database users. + * @enum {string} + */ + GroupType: 'db' | 'oidc'; /** * @description the type of user * @enum {string} @@ -399,6 +416,15 @@ export interface definitions { */ users?: string; }; + /** @description Resources applicable for group actions. */ + groups?: { + /** + * @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. + * @default * + */ + group?: string; + groupType?: definitions['GroupType']; + }; /** @description resources applicable for tenant actions */ tenants?: { /** @@ -496,7 +522,9 @@ export interface definitions { | 'create_aliases' | 'read_aliases' | 'update_aliases' - | 'delete_aliases'; + | 'delete_aliases' + | 'assign_and_revoke_groups' + | 'read_groups'; }; /** @description list of roles */ RolesListResponse: definitions['Role'][]; @@ -1171,8 +1199,6 @@ export interface definitions { BackupListResponse: { /** @description The ID of the backup. Must be URL-safe and work as a filesystem path, only lowercase, numbers, underscore, minus characters allowed. */ id?: string; - /** @description destination path of backup files proper to selected backend */ - path?: string; /** @description The list of classes for which the existed backup process */ classes?: string[]; /** @@ -1191,6 +1217,8 @@ export interface definitions { exclude?: string[]; /** @description Allows overriding the node names stored in the backup with different ones. Useful when restoring backups to a different environment. */ node_mapping?: { [key: string]: string }; + /** @description Allows ovewriting the collection alias if there is a conflict */ + overwriteAlias?: boolean; }; /** @description The definition of a backup restore response body */ BackupRestoreResponse: { @@ -1789,7 +1817,9 @@ export interface definitions { | 'WithinGeoRange' | 'IsNull' | 'ContainsAny' - | 'ContainsAll'; + | 'ContainsAll' + | 'ContainsNone' + | 'Not'; /** * @description path to the property currently being filtered * @example [ @@ -2827,6 +2857,42 @@ export interface operations { }; }; }; + /** Retrieves a list of all groups that have been assigned a specific role, identified by its name. */ + getGroupsForRole: { + parameters: { + path: { + /** The unique name of the role. */ + id: string; + }; + }; + responses: { + /** Successfully retrieved the list of groups that have the role assigned. */ + 200: { + schema: ({ + groupId?: string; + groupType: definitions['GroupType']; + } & { + name: unknown; + })[]; + }; + /** Bad request */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** The specified role was not found. */ + 404: unknown; + /** An error has occurred while trying to fulfill the request. Most likely the ErrorResponse will contain more information about the error. */ + 500: { + schema: definitions['ErrorResponse']; + }; + }; + }; getRolesForUserDeprecated: { parameters: { path: { @@ -2985,6 +3051,7 @@ export interface operations { body: { /** @description the roles that assigned to group */ roles?: string[]; + groupType?: definitions['GroupType']; }; }; }; @@ -3019,6 +3086,7 @@ export interface operations { body: { /** @description the roles that revoked from group */ roles?: string[]; + groupType?: definitions['GroupType']; }; }; }; @@ -3043,6 +3111,80 @@ export interface operations { }; }; }; + /** 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`). */ + getRolesForGroup: { + parameters: { + path: { + /** The unique name of the group. */ + id: string; + /** The type of the group. */ + groupType: 'oidc'; + }; + query: { + /** If true, the response will include the full role definitions with all associated permissions. If false, only role names are returned. */ + includeFullRoles?: boolean; + }; + }; + responses: { + /** A list of roles assigned to the specified group. */ + 200: { + schema: definitions['RolesListResponse']; + }; + /** Bad request */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** The specified group was not found. */ + 404: unknown; + /** The request syntax is correct, but the server couldn't process it due to semantic issues. */ + 422: { + schema: definitions['ErrorResponse']; + }; + /** An error has occurred while trying to fulfill the request. Most likely the ErrorResponse will contain more information about the error. */ + 500: { + schema: definitions['ErrorResponse']; + }; + }; + }; + /** Retrieves a list of all available group names for a specified group type (`oidc` or `db`). */ + getGroups: { + parameters: { + path: { + /** The type of group to retrieve. */ + groupType: 'oidc'; + }; + }; + responses: { + /** A list of group names for the specified type. */ + 200: { + schema: string[]; + }; + /** Bad request */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** The request syntax is correct, but the server couldn't process it due to semantic issues. */ + 422: { + schema: definitions['ErrorResponse']; + }; + /** An error has occurred while trying to fulfill the request. Most likely the ErrorResponse will contain more information about the error. */ + 500: { + schema: definitions['ErrorResponse']; + }; + }; + }; /** Lists all Objects in reverse order of creation, owned by the user that belongs to the used token. */ 'objects.list': { parameters: { From 29bf14bf799c055cbc368afd2d8ececfa5970378 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Thu, 28 Aug 2025 12:57:10 +0200 Subject: [PATCH 2/4] feat: add overwriteAlias parameter to backup.restore --- src/backup/backupRestorer.ts | 7 ++ src/collections/backup/client.ts | 9 +- src/collections/backup/collection.ts | 1 - src/collections/backup/integration.test.ts | 100 +++++++++++++++++++++ src/collections/backup/types.ts | 2 + 5 files changed, 115 insertions(+), 4 deletions(-) diff --git a/src/backup/backupRestorer.ts b/src/backup/backupRestorer.ts index e2f76c0f..011f3e75 100644 --- a/src/backup/backupRestorer.ts +++ b/src/backup/backupRestorer.ts @@ -26,6 +26,7 @@ export default class BackupRestorer extends CommandBase { private statusGetter: BackupRestoreStatusGetter; private waitForCompletion?: boolean; private config?: RestoreConfig; + private overwriteAlias?: boolean; constructor(client: Connection, statusGetter: BackupRestoreStatusGetter) { super(client); @@ -65,6 +66,11 @@ export default class BackupRestorer extends CommandBase { return this; } + withOverwriteAlias(overwriteAlias: boolean) { + this.overwriteAlias = overwriteAlias; + return this; + } + withConfig(cfg: RestoreConfig) { this.config = cfg; return this; @@ -89,6 +95,7 @@ export default class BackupRestorer extends CommandBase { config: this.config, include: this.includeClassNames, exclude: this.excludeClassNames, + overwriteAlias: this.overwriteAlias, } as BackupRestoreRequest; if (this.waitForCompletion) { diff --git a/src/collections/backup/client.ts b/src/collections/backup/client.ts index b24f12bd..79ba8c48 100644 --- a/src/collections/backup/client.ts +++ b/src/collections/backup/client.ts @@ -163,6 +163,9 @@ export const backup = (connection: Connection) => { if (args.excludeCollections) { builder = builder.withExcludeClassNames(...args.excludeCollections); } + if (args.config?.overwriteAlias) { + builder = builder.withOverwriteAlias(args.config?.overwriteAlias); + } if (args.config) { builder = builder.withConfig({ CPUPercentage: args.config.cpuPercentage, @@ -197,9 +200,9 @@ export const backup = (connection: Connection) => { } return status ? { - ...parseResponse(res), - ...status, - } + ...parseResponse(res), + ...status, + } : parseResponse(res); }, }; diff --git a/src/collections/backup/collection.ts b/src/collections/backup/collection.ts index 89ffd864..377d8009 100644 --- a/src/collections/backup/collection.ts +++ b/src/collections/backup/collection.ts @@ -1,6 +1,5 @@ import { Backend } from '../../backup/index.js'; import Connection from '../../connection/index.js'; -import { WeaviateInvalidInputError } from '../../errors.js'; import { backup } from './client.js'; import { BackupReturn, BackupStatusArgs, BackupStatusReturn } from './types.js'; diff --git a/src/collections/backup/integration.test.ts b/src/collections/backup/integration.test.ts index 66b38b2d..1b1905bd 100644 --- a/src/collections/backup/integration.test.ts +++ b/src/collections/backup/integration.test.ts @@ -3,6 +3,8 @@ /* eslint-disable no-await-in-loop */ import { Backend } from '../../backup/index.js'; import weaviate, { Collection, WeaviateClient } from '../../index.js'; +import { requireAtLeast } from '../../../test/version'; +import { WeaviateBackupFailed } from '../../errors.js'; // These must run sequentially because Weaviate is not capable of running multiple backups at the same time describe('Integration testing of backups', () => { @@ -88,6 +90,23 @@ describe('Integration testing of backups', () => { backend: res.backend as 'filesystem', }); expect(status).not.toBe('SUCCESS'); // can be 'STARTED' or 'TRANSFERRING' depending on the speed of the test machine + + // wait to complete so that other tests can run without colliding with Weaviate's lack of simultaneous backups + let wait = true; + while (wait) { + const { status, error } = await collection.backup.getCreateStatus({ + backupId: res.id as string, + backend: res.backend as Backend, + }); + if (status === 'SUCCESS') { + wait = false; + } + if (status === 'FAILED') { + throw new Error(`Backup creation failed: ${error}`); + } + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + return collection; }; @@ -98,6 +117,87 @@ describe('Integration testing of backups', () => { .then(getCollection) .then(testCollectionWaitForCompletion) .then(testCollectionNoWaitForCompletion)); + + requireAtLeast(1, 32, 3).describe('overwrite alias', () => { + + test('overwriteAlias=true', async () => { + const client = await clientPromise; + + const things = await client.collections.create({ name: "ThingsTrue" }); + await client.alias.create({ collection: things.name, alias: `${things.name}Alias` }); + + const backup = await client.backup.create({ + backend: 'filesystem', + backupId: randomBackupId(), + includeCollections: [things.name], + waitForCompletion: true, + }); + + await client.collections.delete(things.name); + await client.alias.delete(`${things.name}Alias`); + + // Change alias to point to a different collection + const inventory = await client.collections.create({ name: "InventoryTrue" }); + await client.alias.create({ collection: inventory.name, alias: `${things.name}Alias` }); + + + // Restore backup with overwriteAlias=true + await client.backup.restore({ + backend: 'filesystem', + backupId: backup.id, + includeCollections: [things.name], + waitForCompletion: true, + config: { overwriteAlias: true }, + }); + + // Assert: alias points to the original collection + const alias = await client.alias.get(`${things.name}Alias`); + expect(alias.collection).toEqual(things.name); + }); + + test('overwriteAlias=false', async () => { + const client = await clientPromise; + + const things = await client.collections.create({ name: "ThingsFalse" }); + await client.alias.create({ collection: things.name, alias: `${things.name}Alias` }); + + const backup = await client.backup.create({ + backend: 'filesystem', + backupId: randomBackupId(), + includeCollections: [things.name], + waitForCompletion: true, + }); + + await client.collections.delete(things.name); + await client.alias.delete(`${things.name}Alias`); + + // Change alias to point to a different collection + const inventory = await client.collections.create({ name: "InventoryFalse" }); + await client.alias.create({ collection: inventory.name, alias: `${things.name}Alias` }); + + + // Restore backup with overwriteAlias=true + const restored = client.backup.restore({ + backend: 'filesystem', + backupId: backup.id, + includeCollections: [things.name], + waitForCompletion: true, + config: { overwriteAlias: false }, + }); + + // Assert: fails with "alias already exists" error + await expect(restored).rejects.toThrowError(WeaviateBackupFailed); + }); + + it('cleanup', async () => { + await clientPromise.then(async c => { + await Promise.all(["ThingsTrue", "ThingsFalse", "InventoryTrue", "InventoryFalse"] + .map(name => c.collections.delete(name).catch(e => { }))); + await c.alias.delete("ThingsFalseAlias").catch(e => { }); + await c.alias.delete("ThingsTrueAlias").catch(e => { }); + }); + }) + }); }); function randomBackupId() { diff --git a/src/collections/backup/types.ts b/src/collections/backup/types.ts index 250d88e6..d76ee18d 100644 --- a/src/collections/backup/types.ts +++ b/src/collections/backup/types.ts @@ -37,6 +37,8 @@ export type BackupConfigCreate = { export type BackupConfigRestore = { /** The percentage of CPU to use for the backuop restoration job. */ cpuPercentage?: number; + /**Allows ovewriting the collection alias if there is a conflict. */ + overwriteAlias?: boolean; }; /** The arguments required to create and restore backups. */ From dedb2970b1557808fb19c8098e7db20328dc7708 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Thu, 28 Aug 2025 13:11:03 +0200 Subject: [PATCH 3/4] chore: target preview docker image --- .github/workflows/main.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 5ebb76b8..8d3cc65a 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -15,7 +15,8 @@ env: WEAVIATE_129: 1.29.8 WEAVIATE_130: 1.30.7 WEAVIATE_131: 1.31.0 - WEAVIATE_132: 1.32.0-rc.1 + WEAVIATE_132: 1.32.4-59530f4.amd64 + concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} From 4668ab4a045b29169fbbb725a7bbf8bf4e8a2d51 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Thu, 28 Aug 2025 13:43:21 +0200 Subject: [PATCH 4/4] chore: lint&format --- src/collections/backup/client.ts | 6 ++--- src/collections/backup/integration.test.ts | 30 +++++++++++----------- src/collections/backup/types.ts | 2 +- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/collections/backup/client.ts b/src/collections/backup/client.ts index 79ba8c48..f11f5868 100644 --- a/src/collections/backup/client.ts +++ b/src/collections/backup/client.ts @@ -200,9 +200,9 @@ export const backup = (connection: Connection) => { } return status ? { - ...parseResponse(res), - ...status, - } + ...parseResponse(res), + ...status, + } : parseResponse(res); }, }; diff --git a/src/collections/backup/integration.test.ts b/src/collections/backup/integration.test.ts index 1b1905bd..2962d082 100644 --- a/src/collections/backup/integration.test.ts +++ b/src/collections/backup/integration.test.ts @@ -1,10 +1,10 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ /* eslint-disable no-await-in-loop */ -import { Backend } from '../../backup/index.js'; -import weaviate, { Collection, WeaviateClient } from '../../index.js'; import { requireAtLeast } from '../../../test/version'; +import { Backend } from '../../backup/index.js'; import { WeaviateBackupFailed } from '../../errors.js'; +import weaviate, { Collection, WeaviateClient } from '../../index.js'; // These must run sequentially because Weaviate is not capable of running multiple backups at the same time describe('Integration testing of backups', () => { @@ -119,11 +119,10 @@ describe('Integration testing of backups', () => { .then(testCollectionNoWaitForCompletion)); requireAtLeast(1, 32, 3).describe('overwrite alias', () => { - test('overwriteAlias=true', async () => { const client = await clientPromise; - const things = await client.collections.create({ name: "ThingsTrue" }); + const things = await client.collections.create({ name: 'ThingsTrue' }); await client.alias.create({ collection: things.name, alias: `${things.name}Alias` }); const backup = await client.backup.create({ @@ -137,10 +136,9 @@ describe('Integration testing of backups', () => { await client.alias.delete(`${things.name}Alias`); // Change alias to point to a different collection - const inventory = await client.collections.create({ name: "InventoryTrue" }); + const inventory = await client.collections.create({ name: 'InventoryTrue' }); await client.alias.create({ collection: inventory.name, alias: `${things.name}Alias` }); - // Restore backup with overwriteAlias=true await client.backup.restore({ backend: 'filesystem', @@ -158,7 +156,7 @@ describe('Integration testing of backups', () => { test('overwriteAlias=false', async () => { const client = await clientPromise; - const things = await client.collections.create({ name: "ThingsFalse" }); + const things = await client.collections.create({ name: 'ThingsFalse' }); await client.alias.create({ collection: things.name, alias: `${things.name}Alias` }); const backup = await client.backup.create({ @@ -172,10 +170,9 @@ describe('Integration testing of backups', () => { await client.alias.delete(`${things.name}Alias`); // Change alias to point to a different collection - const inventory = await client.collections.create({ name: "InventoryFalse" }); + const inventory = await client.collections.create({ name: 'InventoryFalse' }); await client.alias.create({ collection: inventory.name, alias: `${things.name}Alias` }); - // Restore backup with overwriteAlias=true const restored = client.backup.restore({ backend: 'filesystem', @@ -190,13 +187,16 @@ describe('Integration testing of backups', () => { }); it('cleanup', async () => { - await clientPromise.then(async c => { - await Promise.all(["ThingsTrue", "ThingsFalse", "InventoryTrue", "InventoryFalse"] - .map(name => c.collections.delete(name).catch(e => { }))); - await c.alias.delete("ThingsFalseAlias").catch(e => { }); - await c.alias.delete("ThingsTrueAlias").catch(e => { }); + await clientPromise.then(async (c) => { + await Promise.all( + ['ThingsTrue', 'ThingsFalse', 'InventoryTrue', 'InventoryFalse'].map((name) => + c.collections.delete(name).catch((e) => {}) + ) + ); + await c.alias.delete('ThingsFalseAlias').catch((e) => {}); + await c.alias.delete('ThingsTrueAlias').catch((e) => {}); }); - }) + }); }); }); diff --git a/src/collections/backup/types.ts b/src/collections/backup/types.ts index d76ee18d..4403bb0e 100644 --- a/src/collections/backup/types.ts +++ b/src/collections/backup/types.ts @@ -37,7 +37,7 @@ export type BackupConfigCreate = { export type BackupConfigRestore = { /** The percentage of CPU to use for the backuop restoration job. */ cpuPercentage?: number; - /**Allows ovewriting the collection alias if there is a conflict. */ + /** Allows ovewriting the collection alias if there is a conflict. */ overwriteAlias?: boolean; };