mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -05:00
feat(core): add switch of enabling object fully replace when updating DB (#1107)
* feat(core): add switch of enabling object fully replace when updating DB * feat(core): assign default jsonbMode value if possible
This commit is contained in:
parent
93ad8d7769
commit
efa9491749
14 changed files with 78 additions and 29 deletions
|
@ -20,4 +20,5 @@ export type FindManyData<Schema extends SchemaLike> = {
|
|||
export type UpdateWhereData<Schema extends SchemaLike> = {
|
||||
set: Partial<Schema>;
|
||||
where: Partial<Schema>;
|
||||
jsonbMode: 'replace' | 'merge';
|
||||
};
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { CreateUser, Users, Applications } from '@logto/schemas';
|
||||
import { CreateUser, Users, Applications, User } from '@logto/schemas';
|
||||
|
||||
import envSet from '@/env-set';
|
||||
import { UpdateError } from '@/errors/SlonikError';
|
||||
import { createTestPool } from '@/utils/test-utils';
|
||||
|
||||
import { UpdateWhereData } from './types';
|
||||
import { buildUpdateWhere } from './update-where';
|
||||
|
||||
const poolSpy = jest.spyOn(envSet, 'pool', 'get');
|
||||
|
@ -20,6 +21,7 @@ describe('buildUpdateWhere()', () => {
|
|||
updateWhere({
|
||||
set: { username: '123' },
|
||||
where: { id: 'foo', username: '456' },
|
||||
jsonbMode: 'merge',
|
||||
})
|
||||
).resolves.toBe(undefined);
|
||||
});
|
||||
|
@ -47,6 +49,7 @@ describe('buildUpdateWhere()', () => {
|
|||
updateWhere({
|
||||
set: { username: '123', primaryEmail: 'foo@bar.com', applicationId: 'bar' },
|
||||
where: { id: 'foo' },
|
||||
jsonbMode: 'merge',
|
||||
})
|
||||
).resolves.toStrictEqual(user);
|
||||
});
|
||||
|
@ -66,6 +69,7 @@ describe('buildUpdateWhere()', () => {
|
|||
updateWhere({
|
||||
set: { customClientMetadata: { idTokenTtl: 3600 } },
|
||||
where: { id: 'foo' },
|
||||
jsonbMode: 'merge',
|
||||
})
|
||||
).resolves.toStrictEqual({ id: 'foo', customClientMetadata: '{"idTokenTtl":3600}' });
|
||||
});
|
||||
|
@ -82,6 +86,7 @@ describe('buildUpdateWhere()', () => {
|
|||
updateWhere({
|
||||
set: { username: '123', id: undefined },
|
||||
where: { id: 'foo', username: '456' },
|
||||
jsonbMode: 'merge',
|
||||
})
|
||||
).rejects.toMatchError(new Error(`Cannot convert id to primitive`));
|
||||
});
|
||||
|
@ -91,7 +96,11 @@ describe('buildUpdateWhere()', () => {
|
|||
poolSpy.mockReturnValue(pool);
|
||||
|
||||
const updateWhere = buildUpdateWhere(Users, true);
|
||||
const updateWhereData = { set: { username: '123' }, where: { id: 'foo' } };
|
||||
const updateWhereData: UpdateWhereData<User> = {
|
||||
set: { username: '123' },
|
||||
where: { id: 'foo' },
|
||||
jsonbMode: 'merge',
|
||||
};
|
||||
|
||||
await expect(updateWhere(updateWhereData)).rejects.toMatchError(
|
||||
new UpdateError(Users, updateWhereData)
|
||||
|
@ -105,8 +114,14 @@ describe('buildUpdateWhere()', () => {
|
|||
poolSpy.mockReturnValue(pool);
|
||||
|
||||
const updateWhere = buildUpdateWhere(Users, true);
|
||||
const updateData = { set: { username: '123' }, where: { username: 'foo' } };
|
||||
const updateWhereData: UpdateWhereData<User> = {
|
||||
set: { username: '123' },
|
||||
where: { username: 'foo' },
|
||||
jsonbMode: 'merge',
|
||||
};
|
||||
|
||||
await expect(updateWhere(updateData)).rejects.toMatchError(new UpdateError(Users, updateData));
|
||||
await expect(updateWhere(updateWhereData)).rejects.toMatchError(
|
||||
new UpdateError(Users, updateWhereData)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -29,14 +29,14 @@ export const buildUpdateWhere: BuildUpdateWhere = <
|
|||
) => {
|
||||
const { table, fields } = convertToIdentifiers(schema);
|
||||
const isKeyOfSchema = isKeyOf(schema);
|
||||
const connectKeyValueWithEqualSign = (data: Partial<Schema>) =>
|
||||
const connectKeyValueWithEqualSign = (data: Partial<Schema>, jsonbMode: 'replace' | 'merge') =>
|
||||
Object.entries(data)
|
||||
.map(([key, value]) => {
|
||||
if (!isKeyOfSchema(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
if (jsonbMode === 'merge' && value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
/**
|
||||
* Jsonb || operator is used to shallow merge two jsonb types of data
|
||||
* all jsonb data field must be non-nullable
|
||||
|
@ -52,17 +52,17 @@ export const buildUpdateWhere: BuildUpdateWhere = <
|
|||
})
|
||||
.filter((value): value is Truthy<typeof value> => notFalsy(value));
|
||||
|
||||
return async ({ set, where }: UpdateWhereData<Schema>) => {
|
||||
return async ({ set, where, jsonbMode }: UpdateWhereData<Schema>) => {
|
||||
const {
|
||||
rows: [data],
|
||||
} = await envSet.pool.query<ReturnType>(sql`
|
||||
update ${table}
|
||||
set ${sql.join(connectKeyValueWithEqualSign(set), sql`, `)}
|
||||
where ${sql.join(connectKeyValueWithEqualSign(where), sql` and `)}
|
||||
set ${sql.join(connectKeyValueWithEqualSign(set, jsonbMode), sql`, `)}
|
||||
where ${sql.join(connectKeyValueWithEqualSign(where, jsonbMode), sql` and `)}
|
||||
${conditionalSql(returning, () => sql`returning *`)}
|
||||
`);
|
||||
|
||||
assertThat(!returning || data, new UpdateError(schema, { set, where }));
|
||||
assertThat(!returning || data, new UpdateError(schema, { set, where, jsonbMode }));
|
||||
|
||||
return data;
|
||||
};
|
||||
|
|
|
@ -107,9 +107,13 @@ export const verifyPasscode = async (
|
|||
|
||||
if (code !== passcode.code) {
|
||||
// TODO use SQL's native +1
|
||||
await updatePasscode({ where: { id: passcode.id }, set: { tryCount: passcode.tryCount + 1 } });
|
||||
await updatePasscode({
|
||||
where: { id: passcode.id },
|
||||
set: { tryCount: passcode.tryCount + 1 },
|
||||
jsonbMode: 'merge',
|
||||
});
|
||||
throw new RequestError('passcode.code_mismatch');
|
||||
}
|
||||
|
||||
await updatePasscode({ where: { id: passcode.id }, set: { consumed: true } });
|
||||
await updatePasscode({ where: { id: passcode.id }, set: { consumed: true }, jsonbMode: 'merge' });
|
||||
};
|
||||
|
|
|
@ -50,7 +50,11 @@ describe('koaSlonikErrorHandler middleware', () => {
|
|||
});
|
||||
|
||||
it('Update Error', async () => {
|
||||
const error = new UpdateError(Users, { set: { name: 'punk' }, where: { id: '123' } });
|
||||
const error = new UpdateError(Users, {
|
||||
set: { name: 'punk' },
|
||||
where: { id: '123' },
|
||||
jsonbMode: 'merge',
|
||||
});
|
||||
next.mockImplementationOnce(() => {
|
||||
throw error;
|
||||
});
|
||||
|
|
|
@ -43,8 +43,9 @@ const updateApplication = buildUpdateWhere<CreateApplication, Application>(Appli
|
|||
|
||||
export const updateApplicationById = async (
|
||||
id: string,
|
||||
set: Partial<OmitAutoSetFields<CreateApplication>>
|
||||
) => updateApplication({ set, where: { id } });
|
||||
set: Partial<OmitAutoSetFields<CreateApplication>>,
|
||||
jsonbMode: 'replace' | 'merge' = 'merge'
|
||||
) => updateApplication({ set, where: { id }, jsonbMode });
|
||||
|
||||
export const deleteApplicationById = async (id: string) => {
|
||||
const { rowCount } = await envSet.pool.query(sql`
|
||||
|
|
|
@ -80,7 +80,9 @@ describe('connector queries', () => {
|
|||
return createMockQueryResult([{ id, enabled }]);
|
||||
});
|
||||
|
||||
await expect(updateConnector({ where: { id }, set: { enabled } })).resolves.toEqual({
|
||||
await expect(
|
||||
updateConnector({ where: { id }, set: { enabled }, jsonbMode: 'merge' })
|
||||
).resolves.toEqual({
|
||||
id,
|
||||
enabled,
|
||||
});
|
||||
|
|
|
@ -112,7 +112,9 @@ describe('passcode query', () => {
|
|||
return createMockQueryResult([{ ...mockPasscode, tryCount }]);
|
||||
});
|
||||
|
||||
await expect(updatePasscode({ where: { id }, set: { tryCount } })).resolves.toEqual({
|
||||
await expect(
|
||||
updatePasscode({ where: { id }, set: { tryCount }, jsonbMode: 'merge' })
|
||||
).resolves.toEqual({
|
||||
...mockPasscode,
|
||||
tryCount,
|
||||
});
|
||||
|
|
|
@ -49,8 +49,9 @@ const updateResource = buildUpdateWhere<CreateResource, Resource>(Resources, tru
|
|||
|
||||
export const updateResourceById = async (
|
||||
id: string,
|
||||
set: Partial<OmitAutoSetFields<CreateResource>>
|
||||
) => updateResource({ set, where: { id } });
|
||||
set: Partial<OmitAutoSetFields<CreateResource>>,
|
||||
jsonbMode: 'replace' | 'merge' = 'merge'
|
||||
) => updateResource({ set, where: { id }, jsonbMode });
|
||||
|
||||
export const deleteResourceById = async (id: string) => {
|
||||
const { rowCount } = await envSet.pool.query(sql`
|
||||
|
|
|
@ -16,9 +16,12 @@ export const getSetting = async () =>
|
|||
where ${fields.id}=${defaultSettingId}
|
||||
`);
|
||||
|
||||
export const updateSetting = async (setting: Partial<OmitAutoSetFields<CreateSetting>>) => {
|
||||
export const updateSetting = async (
|
||||
setting: Partial<OmitAutoSetFields<CreateSetting>>,
|
||||
jsonbMode: 'replace' | 'merge' = 'merge'
|
||||
) => {
|
||||
return buildUpdateWhere<CreateSetting, Setting>(
|
||||
Settings,
|
||||
true
|
||||
)({ set: setting, where: { id: defaultSettingId } });
|
||||
)({ set: setting, where: { id: defaultSettingId }, jsonbMode });
|
||||
};
|
||||
|
|
|
@ -14,8 +14,10 @@ const updateSignInExperience = buildUpdateWhere<CreateSignInExperience, SignInEx
|
|||
|
||||
const id = 'default';
|
||||
|
||||
export const updateDefaultSignInExperience = async (set: Partial<CreateSignInExperience>) =>
|
||||
updateSignInExperience({ set, where: { id } });
|
||||
export const updateDefaultSignInExperience = async (
|
||||
set: Partial<CreateSignInExperience>,
|
||||
jsonbMode: 'replace' | 'merge' = 'merge'
|
||||
) => updateSignInExperience({ set, where: { id }, jsonbMode });
|
||||
|
||||
export const findDefaultSignInExperience = async () =>
|
||||
envSet.pool.one<SignInExperience>(sql`
|
||||
|
|
|
@ -114,8 +114,11 @@ export const findUsers = async (limit: number, offset: number, search?: string)
|
|||
|
||||
const updateUser = buildUpdateWhere<CreateUser, User>(Users, true);
|
||||
|
||||
export const updateUserById = async (id: string, set: Partial<OmitAutoSetFields<CreateUser>>) =>
|
||||
updateUser({ set, where: { id } });
|
||||
export const updateUserById = async (
|
||||
id: string,
|
||||
set: Partial<OmitAutoSetFields<CreateUser>>,
|
||||
jsonbMode: 'replace' | 'merge' = 'merge'
|
||||
) => updateUser({ set, where: { id }, jsonbMode });
|
||||
|
||||
export const deleteUserById = async (id: string) => {
|
||||
const { rowCount } = await envSet.pool.query(sql`
|
||||
|
@ -136,7 +139,7 @@ export const clearUserCustomDataById = async (id: string) => {
|
|||
`);
|
||||
|
||||
if (rowCount < 1) {
|
||||
throw new UpdateError(Users, { set: { customData: {} }, where: { id } });
|
||||
throw new UpdateError(Users, { set: { customData: {} }, where: { id }, jsonbMode: 'replace' });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -108,12 +108,16 @@ export default function connectorRoutes<T extends AuthedRouter>(router: T) {
|
|||
connector.metadata.type === metadata.type && connector.connector.enabled
|
||||
)
|
||||
.map(async ({ connector: { id } }) =>
|
||||
updateConnector({ set: { enabled: false }, where: { id } })
|
||||
updateConnector({ set: { enabled: false }, where: { id }, jsonbMode: 'merge' })
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const connector = await updateConnector({ set: { enabled }, where: { id } });
|
||||
const connector = await updateConnector({
|
||||
set: { enabled },
|
||||
where: { id },
|
||||
jsonbMode: 'merge',
|
||||
});
|
||||
ctx.body = { ...connector, metadata };
|
||||
|
||||
return next();
|
||||
|
@ -137,7 +141,7 @@ export default function connectorRoutes<T extends AuthedRouter>(router: T) {
|
|||
await validateConfig(body.config);
|
||||
}
|
||||
|
||||
const connector = await updateConnector({ set: body, where: { id } });
|
||||
const connector = await updateConnector({ set: body, where: { id }, jsonbMode: 'replace' });
|
||||
ctx.body = { ...connector, metadata };
|
||||
|
||||
return next();
|
||||
|
|
|
@ -88,6 +88,7 @@ describe('connector PATCH routes', () => {
|
|||
expect.objectContaining({
|
||||
where: { id: 'id' },
|
||||
set: { enabled: true },
|
||||
jsonbMode: 'merge',
|
||||
})
|
||||
);
|
||||
expect(response.body).toMatchObject({
|
||||
|
@ -126,6 +127,7 @@ describe('connector PATCH routes', () => {
|
|||
expect.objectContaining({
|
||||
where: { id: 'id' },
|
||||
set: { enabled: false },
|
||||
jsonbMode: 'merge',
|
||||
})
|
||||
);
|
||||
expect(response.body).toMatchObject({
|
||||
|
@ -160,6 +162,7 @@ describe('connector PATCH routes', () => {
|
|||
expect.objectContaining({
|
||||
where: { id: 'id1' },
|
||||
set: { enabled: false },
|
||||
jsonbMode: 'merge',
|
||||
})
|
||||
);
|
||||
expect(updateConnector).toHaveBeenNthCalledWith(
|
||||
|
@ -167,6 +170,7 @@ describe('connector PATCH routes', () => {
|
|||
expect.objectContaining({
|
||||
where: { id: 'id5' },
|
||||
set: { enabled: false },
|
||||
jsonbMode: 'merge',
|
||||
})
|
||||
);
|
||||
expect(updateConnector).toHaveBeenNthCalledWith(
|
||||
|
@ -174,6 +178,7 @@ describe('connector PATCH routes', () => {
|
|||
expect.objectContaining({
|
||||
where: { id: 'id1' },
|
||||
set: { enabled: true },
|
||||
jsonbMode: 'merge',
|
||||
})
|
||||
);
|
||||
expect(response.body).toMatchObject({
|
||||
|
@ -214,6 +219,7 @@ describe('connector PATCH routes', () => {
|
|||
expect.objectContaining({
|
||||
where: { id: 'id' },
|
||||
set: { enabled: false },
|
||||
jsonbMode: 'merge',
|
||||
})
|
||||
);
|
||||
expect(response.body).toMatchObject({
|
||||
|
@ -270,6 +276,7 @@ describe('connector PATCH routes', () => {
|
|||
expect.objectContaining({
|
||||
where: { id: 'id' },
|
||||
set: { config: { cliend_id: 'client_id', client_secret: 'client_secret' } },
|
||||
jsonbMode: 'replace',
|
||||
})
|
||||
);
|
||||
expect(response.body).toMatchObject({
|
||||
|
|
Loading…
Reference in a new issue