From 1b1150da72cb629eb5052c5362912e7df2591f83 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Tue, 15 Apr 2025 18:17:57 +0200 Subject: [PATCH 1/7] chore: refresh OpenAPI schema --- src/openapi/schema.ts | 88 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/src/openapi/schema.ts b/src/openapi/schema.ts index b5c11aa5..56a18d78 100644 --- a/src/openapi/schema.ts +++ b/src/openapi/schema.ts @@ -43,6 +43,10 @@ export interface paths { '/replication/replicate': { post: operations['replicate']; }; + '/replication/replicate/{id}': { + /** Returns the details of a replication operation for a given shard, identified by the provided replication operation id. */ + get: operations['replicationDetails']; + }; '/users/own-info': { get: operations['getOwnInfo']; }; @@ -286,6 +290,18 @@ export interface definitions { dbUserType: 'db_user' | 'db_env_user'; /** @description activity status of the returned user */ active: boolean; + /** + * Format: date-time + * @description Date and time in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ) + */ + createdAt?: unknown; + /** @description First 3 letters of the associated API-key */ + apiKeyFirstLetters?: unknown; + /** + * Format: date-time + * @description Date and time in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ) + */ + lastUsedAt?: unknown; }; UserApiKey: { /** @description The apikey */ @@ -568,6 +584,8 @@ export interface definitions { indexNullState?: boolean; /** @description Index length of properties (default: 'false'). */ indexPropertyLength?: boolean; + /** @description Using BlockMax WAND for query execution (default: 'false', will be 'true' for new collections created after 1.30). */ + usingBlockMaxWAND?: boolean; }; /** @description Configure how replication is executed in a cluster */ ReplicationConfig: { @@ -689,6 +707,29 @@ export interface definitions { /** @description The shard id holding the replica to be deleted */ shardId: string; }; + /** @description The current status and details of a replication operation, including information about the resources involved in the replication process. */ + ReplicationReplicateDetailsReplicaResponse: { + /** @description The unique id of the replication operation. */ + id: string; + /** @description The id of the shard to collect replication details for. */ + shardId: string; + /** @description The name of the collection holding data being replicated. */ + collection: string; + /** @description The id of the node where the source replica is allocated. */ + sourceNodeId: string; + /** @description The id of the node where the target replica is allocated. */ + targetNodeId: string; + /** + * @description The current status of the replication operation, indicating the replication phase the operation is in. + * @enum {string} + */ + status: + | 'READY' + | 'INDEXING' + | 'REPLICATION_FINALIZING' + | 'REPLICATION_HYDRATING' + | 'REPLICATION_DEHYDRATING'; + }; /** @description A single peer in the network. */ PeerUpdate: { /** @@ -1728,6 +1769,37 @@ export interface operations { }; }; }; + /** Returns the details of a replication operation for a given shard, identified by the provided replication operation id. */ + replicationDetails: { + parameters: { + path: { + /** The replication operation id to get details for. */ + id: string; + }; + }; + responses: { + /** The details of the replication operation. */ + 200: { + schema: definitions['ReplicationReplicateDetailsReplicaResponse']; + }; + /** Malformed request. */ + 400: { + schema: definitions['ErrorResponse']; + }; + /** Unauthorized or invalid credentials. */ + 401: unknown; + /** Forbidden */ + 403: { + schema: definitions['ErrorResponse']; + }; + /** Shard replica operation 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']; + }; + }; + }; getOwnInfo: { responses: { /** Info about the user */ @@ -1743,8 +1815,14 @@ export interface operations { }; }; listAllUsers: { + parameters: { + query: { + /** Whether to include the last used time of the users */ + includeLastUsedTime?: boolean; + }; + }; responses: { - /** Info about the user */ + /** Info about the users */ 200: { schema: definitions['DBUserInfo'][]; }; @@ -1766,6 +1844,10 @@ export interface operations { /** user id */ user_id: string; }; + query: { + /** Whether to include the last used time of the given user */ + includeLastUsedTime?: boolean; + }; }; responses: { /** Info about the user */ @@ -1780,6 +1862,10 @@ export interface operations { }; /** user not found */ 404: unknown; + /** Request body is well-formed (i.e., syntactically correct), but semantically erroneous. */ + 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']; From 42f5074cfb2c82aa4a80ac80fba43325d3979b5a Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Tue, 15 Apr 2025 18:18:29 +0200 Subject: [PATCH 2/7] feat: request additional user info --- src/roles/util.ts | 5 +++++ src/users/index.ts | 14 ++++++------- src/users/integration.test.ts | 38 ++++++++++++++++++++++++++++++++++- src/users/types.ts | 7 +++++++ 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/roles/util.ts b/src/roles/util.ts index 09f82bb0..cb7ca68e 100644 --- a/src/roles/util.ts +++ b/src/roles/util.ts @@ -155,6 +155,8 @@ export class Map { id: user.userId, roleNames: user.roles, active: user.active, + createdAt: Map.unknownDate(user.createdAt), + lastUsedAt: Map.unknownDate(user.lastUsedAt), }); static dbUsers = (users: WeaviateDBUser[]): UserDB[] => users.map(Map.dbUser); static assignedUsers = (users: WeaviateAssignedUser[]): UserAssignment[] => @@ -162,6 +164,9 @@ export class Map { id: user.userId || '', userType: user.userType, })); + static unknownDate = (date?: unknown): Date | undefined => ( + date !== undefined && typeof date === "string" + ? new Date(date) : undefined); } class PermissionsMapping { diff --git a/src/users/index.ts b/src/users/index.ts index bc11ef2b..d3fcf33e 100644 --- a/src/users/index.ts +++ b/src/users/index.ts @@ -8,7 +8,7 @@ import { } from '../openapi/types.js'; import { Role } from '../roles/types.js'; import { Map } from '../roles/util.js'; -import { AssignRevokeOptions, DeactivateOptions, GetAssignedRolesOptions, User, UserDB } from './types.js'; +import { AssignRevokeOptions, DeactivateOptions, GetAssignedRolesOptions, GetUserOptions, User, UserDB } from './types.js'; /** * Operations supported for 'db', 'oidc', and legacy (non-namespaced) users. @@ -116,14 +116,14 @@ export interface DBUsers extends UsersBase { * @param {string} userId The ID of the user to get. * @returns {Promise} ID, status, and assigned roles of a 'db_*' user. */ - byName: (userId: string) => Promise; + byName: (userId: string, opts?: GetUserOptions) => Promise; /** * List all 'db_user' / 'db_env_user' users. * * @returns {Promise} ID, status, and assigned roles for each 'db_*' user. */ - listAll: () => Promise; + listAll: (opts?: GetUserOptions) => Promise; } /** Operations supported for namespaced 'oidc' users.*/ @@ -195,8 +195,8 @@ const db = (connection: ConnectionREST): DBUsers => { .postEmpty(`/users/db/${userId}/deactivate`, opts || null) .then(() => true) .catch(expectCode(409)), - byName: (userId: string) => connection.get(`/users/db/${userId}`, true).then(Map.dbUser), - listAll: () => connection.get('/users/db', true).then(Map.dbUsers), + byName: (userId: string, opts?: GetUserOptions) => connection.get(`/users/db/${userId}?includeLastUsedTime=${opts?.includeLastUsedTime || false}`, true).then(Map.dbUser), + listAll: (opts?: GetUserOptions) => connection.get(`/users/db?includeLastUsedTime=${opts?.includeLastUsedTime || false}`, true).then(Map.dbUsers), }; }; @@ -238,9 +238,7 @@ const namespacedUsers = (connection: ConnectionREST): NamespacedUsers => { getAssignedRoles: (userType: UserTypeInternal, userId: string, opts?: GetAssignedRolesOptions) => connection .get( - `/authz/users/${userId}/roles/${userType}${ - opts?.includePermissions ? '?&includeFullRoles=true' : '' - }` + `/authz/users/${userId}/roles/${userType}?includeFullRoles=${opts?.includePermissions || false}` ) .then(Map.roles), assignRoles: (roleNames: string | string[], userId: string, opts?: AssignRevokeOptions) => diff --git a/src/users/integration.test.ts b/src/users/integration.test.ts index 809b74e7..cfd5feaf 100644 --- a/src/users/integration.test.ts +++ b/src/users/integration.test.ts @@ -160,10 +160,46 @@ requireAtLeast( expect(roles.test.nodesPermissions).toHaveLength(1); }); + requireAtLeast(1, 30, 1)('additional DUM features', () => { + it('should be able to fetch additional user info', async () => { + const admin = await makeClient('admin-key'); + const timKey = await admin.users.db.create('timely-tim'); + + // Get user info with / without lastUserTime + let timUser = await admin.users.db.byName('timely-tim'); + expect(timUser.createdAt).not.toBeUndefined(); // always returned + expect(timUser.lastUsedAt).toBeUndefined(); + + await expect(admin.users.db.byName('timely-tim', { includeLastUsedTime: true })) + .resolves.toEqual(expect.any(Date)); + + // apiKeyFirstLetters contain first letters of the API key + expect(timUser.apiKeyFirstLetters).not.toBeUndefined(); + expect(timKey).toMatch(new RegExp(`^${timUser.apiKeyFirstLetters}.*`)) + + // Check that Tim cannnot see the first letters of the API key + const tim = await makeClient(timKey); + expect(await tim.users.getMyUser()).resolves.toHaveProperty('apiKeyFirstLetters', undefined); + }); + + it('should be able to list all users with additional info', async () => { + const admin = await makeClient('admin-key'); + await admin.users.db.listAll({ includeLastUsedTime: true }).then(res => { + res.forEach((user, _) => { + expect(user).toEqual(expect.objectContaining({ + createdAt: expect.any(Date), + lastUsedAt: expect.any(Date), + apiKeyFirstLetters: expect.any(String), + })); + }); + }); + }); + }) + afterAll(() => makeClient('admin-key').then(async (c) => { await Promise.all( - ['jim', 'pam', 'dwight', 'dynamic-dave', 'api-ashley', 'role-rick', 'permission-peter'].map((n) => + ['jim', 'pam', 'dwight', 'dynamic-dave', 'api-ashley', 'role-rick', 'permission-peter', 'timely-tim'].map((n) => c.users.db.delete(n) ) ); diff --git a/src/users/types.ts b/src/users/types.ts index b4a9d59d..1bf2b46b 100644 --- a/src/users/types.ts +++ b/src/users/types.ts @@ -11,6 +11,10 @@ export type UserDB = { id: string; roleNames: string[]; active: boolean; + + createdAt?: Date; + lastUsedAt?: Date; + apiKeyFirstLetters?: string; }; /** Optional arguments to /user/{type}/{username} enpoint. */ @@ -23,3 +27,6 @@ export type AssignRevokeOptions = { userType?: WeaviateUserTypeInternal }; /** Optional arguments to /deactivate endpoint. */ export type DeactivateOptions = { revokeKey?: boolean }; + +/** Optional arguments to /users and /users/ endpoints. */ +export type GetUserOptions = { includeLastUsedTime?: boolean }; From 84856088ac30af7d74f2234a557e995fae26bed2 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Thu, 17 Apr 2025 10:16:09 +0200 Subject: [PATCH 3/7] fix: replace zero time with undefined --- src/roles/util.ts | 9 ++- src/users/index.ts | 22 ++++++- src/users/integration.test.ts | 114 ++++++++++++++++++++++------------ 3 files changed, 99 insertions(+), 46 deletions(-) diff --git a/src/roles/util.ts b/src/roles/util.ts index cb7ca68e..7bf42444 100644 --- a/src/roles/util.ts +++ b/src/roles/util.ts @@ -29,6 +29,9 @@ import { UsersPermission, } from './types.js'; +/** ZERO_TIME is the timestamp Weaviate server sends in abscence of a value (null value). */ +const ZERO_TIME = '0001-01-01T00:00:00.000Z'; + export class PermissionGuards { private static includes = (permission: Permission, ...actions: A[]): boolean => actions.filter((a) => Array.from(permission.actions).includes(a)).length > 0; @@ -157,6 +160,7 @@ export class Map { active: user.active, createdAt: Map.unknownDate(user.createdAt), lastUsedAt: Map.unknownDate(user.lastUsedAt), + apiKeyFirstLetters: user.apiKeyFirstLetters as string, }); static dbUsers = (users: WeaviateDBUser[]): UserDB[] => users.map(Map.dbUser); static assignedUsers = (users: WeaviateAssignedUser[]): UserAssignment[] => @@ -164,9 +168,8 @@ export class Map { id: user.userId || '', userType: user.userType, })); - static unknownDate = (date?: unknown): Date | undefined => ( - date !== undefined && typeof date === "string" - ? new Date(date) : undefined); + static unknownDate = (date?: unknown): Date | undefined => + date !== undefined && typeof date === 'string' && date !== ZERO_TIME ? new Date(date) : undefined; } class PermissionsMapping { diff --git a/src/users/index.ts b/src/users/index.ts index d3fcf33e..3bae45f5 100644 --- a/src/users/index.ts +++ b/src/users/index.ts @@ -8,7 +8,14 @@ import { } from '../openapi/types.js'; import { Role } from '../roles/types.js'; import { Map } from '../roles/util.js'; -import { AssignRevokeOptions, DeactivateOptions, GetAssignedRolesOptions, GetUserOptions, User, UserDB } from './types.js'; +import { + AssignRevokeOptions, + DeactivateOptions, + GetAssignedRolesOptions, + GetUserOptions, + User, + UserDB, +} from './types.js'; /** * Operations supported for 'db', 'oidc', and legacy (non-namespaced) users. @@ -195,8 +202,17 @@ const db = (connection: ConnectionREST): DBUsers => { .postEmpty(`/users/db/${userId}/deactivate`, opts || null) .then(() => true) .catch(expectCode(409)), - byName: (userId: string, opts?: GetUserOptions) => connection.get(`/users/db/${userId}?includeLastUsedTime=${opts?.includeLastUsedTime || false}`, true).then(Map.dbUser), - listAll: (opts?: GetUserOptions) => connection.get(`/users/db?includeLastUsedTime=${opts?.includeLastUsedTime || false}`, true).then(Map.dbUsers), + byName: (userId: string, opts?: GetUserOptions) => + connection + .get( + `/users/db/${userId}?includeLastUsedTime=${opts?.includeLastUsedTime || false}`, + true + ) + .then(Map.dbUser), + listAll: (opts?: GetUserOptions) => + connection + .get(`/users/db?includeLastUsedTime=${opts?.includeLastUsedTime || false}`, true) + .then(Map.dbUsers), }; }; diff --git a/src/users/integration.test.ts b/src/users/integration.test.ts index cfd5feaf..16e2a22b 100644 --- a/src/users/integration.test.ts +++ b/src/users/integration.test.ts @@ -1,6 +1,6 @@ import weaviate, { ApiKey } from '..'; import { requireAtLeast } from '../../test/version.js'; -import { WeaviateUserTypeDB } from '../v2'; +import { WeaviateUserTypeDB } from '../openapi/types.js'; import { UserDB } from './types.js'; requireAtLeast( @@ -16,9 +16,10 @@ requireAtLeast( }); beforeAll(() => - makeClient('admin-key').then((c) => - c.roles.create('test', weaviate.permissions.data({ collection: 'Thing', read: true })) - ) + makeClient('admin-key').then((c) => { + c.roles.delete('test'); + c.roles.create('test', weaviate.permissions.data({ collection: 'Thing', read: true })); + }) ); it('should be able to retrieve own admin user with root roles', async () => { @@ -67,34 +68,34 @@ requireAtLeast( 0 )('dynamic user management', () => { it('should be able to manage "db" user lifecycle', async () => { - const client = await makeClient('admin-key'); + const admin = await makeClient('admin-key'); /** Pass false to expect a rejected promise, chain assertions about dynamic-dave otherwise. */ const expectDave = (ok: boolean = true) => { - const promise = expect(client.users.db.byName('dynamic-dave')); + const promise = expect(admin.users.db.byName('dynamic-dave')); return ok ? promise.resolves : promise.rejects; }; - await client.users.db.create('dynamic-dave'); + await admin.users.db.create('dynamic-dave'); await expectDave().toHaveProperty('active', true); // Second activation is a no-op - await expect(client.users.db.activate('dynamic-dave')).resolves.toEqual(false); + await expect(admin.users.db.activate('dynamic-dave')).resolves.toEqual(false); - await expect(client.users.db.deactivate('dynamic-dave')).resolves.toEqual(true); + await expect(admin.users.db.deactivate('dynamic-dave')).resolves.toEqual(true); await expectDave().toHaveProperty('active', false); // Second deactivation is a no-op - await expect(client.users.db.deactivate('dynamic-dave', { revokeKey: true })).resolves.toEqual(false); + await expect(admin.users.db.deactivate('dynamic-dave', { revokeKey: true })).resolves.toEqual(false); // Re-activate - await expect(client.users.db.activate('dynamic-dave')).resolves.toEqual(true); + await expect(admin.users.db.activate('dynamic-dave')).resolves.toEqual(true); - await expect(client.users.db.delete('dynamic-dave')).resolves.toEqual(true); + await expect(admin.users.db.delete('dynamic-dave')).resolves.toEqual(true); await expectDave(false).toHaveProperty('code', 404); // Second deletion is a no-op - await expect(client.users.db.delete('dynamic-dave')).resolves.toEqual(false); + await expect(admin.users.db.delete('dynamic-dave')).resolves.toEqual(false); }); it('should be able to obtain and rotate api keys', async () => { @@ -145,63 +146,96 @@ requireAtLeast( it('should be able to fetch assigned roles with all permissions', async () => { const admin = await makeClient('admin-key'); - await admin.roles.delete('test'); - await admin.roles.create('test', [ + await admin.roles.delete('Permissioner'); + await admin.roles.create('Permissioner', [ { collection: 'Things', actions: ['manage_backups'] }, { collection: 'Things', tenant: 'data-tenant', actions: ['create_data'] }, { collection: 'Things', verbosity: 'minimal', actions: ['read_nodes'] }, ]); await admin.users.db.create('permission-peter'); - await admin.users.db.assignRoles('test', 'permission-peter'); + await admin.users.db.assignRoles('Permissioner', 'permission-peter'); const roles = await admin.users.db.getAssignedRoles('permission-peter', { includePermissions: true }); - expect(roles.test.backupsPermissions).toHaveLength(1); - expect(roles.test.dataPermissions).toHaveLength(1); - expect(roles.test.nodesPermissions).toHaveLength(1); + expect(roles.Permissioner.backupsPermissions).toHaveLength(1); + expect(roles.Permissioner.dataPermissions).toHaveLength(1); + expect(roles.Permissioner.nodesPermissions).toHaveLength(1); }); - requireAtLeast(1, 30, 1)('additional DUM features', () => { + requireAtLeast( + 1, + 30, + 1 + )('additional DUM features', () => { it('should be able to fetch additional user info', async () => { const admin = await makeClient('admin-key'); const timKey = await admin.users.db.create('timely-tim'); + // Allow timely-tim to read own role + await admin.roles.delete('TimReader'); + await admin.roles.create('TimReader', [{ actions: ['read_users'], users: 'timely-tim' }]); + await admin.users.db.assignRoles('TimReader', 'timely-tim'); + // Get user info with / without lastUserTime - let timUser = await admin.users.db.byName('timely-tim'); + const timUser = await admin.users.db.byName('timely-tim'); expect(timUser.createdAt).not.toBeUndefined(); // always returned expect(timUser.lastUsedAt).toBeUndefined(); - - await expect(admin.users.db.byName('timely-tim', { includeLastUsedTime: true })) - .resolves.toEqual(expect.any(Date)); - - // apiKeyFirstLetters contain first letters of the API key expect(timUser.apiKeyFirstLetters).not.toBeUndefined(); - expect(timKey).toMatch(new RegExp(`^${timUser.apiKeyFirstLetters}.*`)) // Check that Tim cannnot see the first letters of the API key const tim = await makeClient(timKey); - expect(await tim.users.getMyUser()).resolves.toHaveProperty('apiKeyFirstLetters', undefined); + await expect(tim.users.db.byName('timely-tim')).resolves.toHaveProperty( + 'apiKeyFirstLetters', + undefined + ); + + await expect(admin.users.db.byName('timely-tim', { includeLastUsedTime: true })).resolves.toEqual( + expect.objectContaining({ lastUsedAt: expect.any(Date) }) + ); + + // apiKeyFirstLetters contain first letters of the API key + expect(timKey).toMatch(new RegExp(`^${timUser.apiKeyFirstLetters}.*`)); }); it('should be able to list all users with additional info', async () => { const admin = await makeClient('admin-key'); - await admin.users.db.listAll({ includeLastUsedTime: true }).then(res => { - res.forEach((user, _) => { - expect(user).toEqual(expect.objectContaining({ - createdAt: expect.any(Date), - lastUsedAt: expect.any(Date), - apiKeyFirstLetters: expect.any(String), - })); - }); + + await admin.users.db.listAll({ includeLastUsedTime: true }).then((res) => { + res + // Users created in environment (db_env_user) do not have + // createdAt and apiKeyFirstLetters fields. + .filter((user) => user.userType == 'db_user') + .map(async (user) => { + // Use each user at least once + const key = await admin.users.db.rotateKey(user.id); + await makeClient(key).then((c) => c.users.getMyUser()); + return user; + }) + .forEach(async (user, _) => + expect(await user).toEqual( + expect.objectContaining({ + createdAt: expect.any(Date), + lastUsedAt: expect.any(Date), + apiKeyFirstLetters: expect.any(String), + }) + ) + ); }); }); - }) + }); afterAll(() => makeClient('admin-key').then(async (c) => { await Promise.all( - ['jim', 'pam', 'dwight', 'dynamic-dave', 'api-ashley', 'role-rick', 'permission-peter', 'timely-tim'].map((n) => - c.users.db.delete(n) - ) + [ + 'jim', + 'pam', + 'dwight', + 'dynamic-dave', + 'api-ashley', + 'role-rick', + 'permission-peter', + 'timely-tim', + ].map((n) => c.users.db.delete(n)) ); }) ); From 0d2b570901ab7bc894c59913f9ccec6baf17571e Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Thu, 17 Apr 2025 12:52:31 +0200 Subject: [PATCH 4/7] test: fix flaky test --- src/users/integration.test.ts | 73 +++++++++++++++++------------------ 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/src/users/integration.test.ts b/src/users/integration.test.ts index 16e2a22b..d148542a 100644 --- a/src/users/integration.test.ts +++ b/src/users/integration.test.ts @@ -1,7 +1,7 @@ -import weaviate, { ApiKey } from '..'; +import weaviate, { ApiKey, WeaviateClient } from '..'; import { requireAtLeast } from '../../test/version.js'; import { WeaviateUserTypeDB } from '../openapi/types.js'; -import { UserDB } from './types.js'; +import { GetUserOptions, UserDB } from './types.js'; requireAtLeast( 1, @@ -67,6 +67,17 @@ requireAtLeast( 30, 0 )('dynamic user management', () => { + /** List dynamic DB users. */ + const listDBUsers = (c: WeaviateClient, opts?: GetUserOptions) => + c.users.db.listAll(opts).then((all) => all.filter((u) => u.userType == 'db_user')); + + const deleteAllUsers = async (c: WeaviateClient) => { + const users = await listDBUsers(c); + await Promise.all(users.map((user) => c.users.db.delete(user.id))); + }; + + beforeAll(() => makeClient('admin-key').then(deleteAllUsers)); + it('should be able to manage "db" user lifecycle', async () => { const admin = await makeClient('admin-key'); @@ -199,46 +210,32 @@ requireAtLeast( it('should be able to list all users with additional info', async () => { const admin = await makeClient('admin-key'); - await admin.users.db.listAll({ includeLastUsedTime: true }).then((res) => { - res - // Users created in environment (db_env_user) do not have - // createdAt and apiKeyFirstLetters fields. - .filter((user) => user.userType == 'db_user') - .map(async (user) => { - // Use each user at least once - const key = await admin.users.db.rotateKey(user.id); - await makeClient(key).then((c) => c.users.getMyUser()); - return user; + // Create test users and use each at least once + const created = await Promise.all( + ['A', 'B', 'C'].map(async (user) => { + const key = await admin.users.db.create(user); + await makeClient(key).then((c) => c.users.getMyUser()); + return user; + }) + ); + + const users = await listDBUsers(admin, { includeLastUsedTime: true }).then((users) => + users.filter((user) => created.includes(user.id)) + ); + + users.forEach((user) => + expect(user).toEqual( + expect.objectContaining({ + createdAt: expect.any(Date), + lastUsedAt: expect.any(Date), + apiKeyFirstLetters: expect.any(String), }) - .forEach(async (user, _) => - expect(await user).toEqual( - expect.objectContaining({ - createdAt: expect.any(Date), - lastUsedAt: expect.any(Date), - apiKeyFirstLetters: expect.any(String), - }) - ) - ); - }); + ) + ); }); }); - afterAll(() => - makeClient('admin-key').then(async (c) => { - await Promise.all( - [ - 'jim', - 'pam', - 'dwight', - 'dynamic-dave', - 'api-ashley', - 'role-rick', - 'permission-peter', - 'timely-tim', - ].map((n) => c.users.db.delete(n)) - ); - }) - ); + afterAll(() => makeClient('admin-key').then(deleteAllUsers)); }); afterAll(() => makeClient('admin-key').then((c) => c.roles.delete('test'))); From 1f0e7d4978a6fae0c85e48d6247c095f72f7bfe6 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Thu, 17 Apr 2025 15:33:33 +0200 Subject: [PATCH 5/7] ci: target 1.30.1 --- .github/workflows/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index fb393a03..69c5bdd5 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -13,7 +13,7 @@ env: WEAVIATE_127: 1.27.15 WEAVIATE_128: 1.28.11 WEAVIATE_129: 1.29.1 - WEAVIATE_130: 1.30.0 + WEAVIATE_130: 1.30.1 concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} From 2844223d8c1d7eb31cf90d9152981e8232ef0843 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Thu, 17 Apr 2025 16:35:56 +0200 Subject: [PATCH 6/7] ci(temp): tag alpha release --- .github/workflows/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 69c5bdd5..5b0898c4 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -125,7 +125,7 @@ jobs: registry-url: 'https://registry.npmjs.org' - run: npm ci - run: npm run build - - run: npm publish + - run: npm publish --tag alpha env: NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTOMATION_TOKEN }} - run: npm run docs From 9df0dcae2dbc1e87fa0b45284a51591795784023 Mon Sep 17 00:00:00 2001 From: dyma solovei Date: Tue, 6 May 2025 13:10:17 +0200 Subject: [PATCH 7/7] ci: remove '--tag alpha' option from publish command --- .github/workflows/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 5b0898c4..69c5bdd5 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -125,7 +125,7 @@ jobs: registry-url: 'https://registry.npmjs.org' - run: npm ci - run: npm run build - - run: npm publish --tag alpha + - run: npm publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTOMATION_TOKEN }} - run: npm run docs