0
Fork 0
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:
Darcy Ye 2022-06-14 21:38:10 +08:00 committed by GitHub
parent 93ad8d7769
commit efa9491749
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 78 additions and 29 deletions

View file

@ -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';
};

View file

@ -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)
);
});
});

View file

@ -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;
};

View file

@ -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' });
};

View file

@ -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;
});

View file

@ -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`

View file

@ -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,
});

View file

@ -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,
});

View file

@ -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`

View file

@ -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 });
};

View file

@ -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`

View file

@ -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' });
}
};

View file

@ -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();

View file

@ -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({