From 76bcd3c52c0a5913a6e3c31a1a202ac8b00c1b50 Mon Sep 17 00:00:00 2001 From: Denis Bykhov <bykhov.denis@gmail.com> Date: Tue, 12 Nov 2024 22:40:15 +0500 Subject: [PATCH] Improve calendar schema (#7156) Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> --- models/calendar/src/index.ts | 3 +- models/calendar/src/migration.ts | 12 +++- .../src/components/review/CreateReview.svelte | 8 +-- server/postgres/migrations/calendarSchema.sql | 5 ++ server/postgres/migrations/eventSchema.sql | 19 ++++++ server/postgres/migrations/timeSchema.sql | 17 ++++++ server/postgres/src/schemas.ts | 60 +++++++++++++++++++ server/postgres/src/storage.ts | 32 ++++++---- server/postgres/src/utils.ts | 22 ++++--- 9 files changed, 151 insertions(+), 27 deletions(-) create mode 100644 server/postgres/migrations/calendarSchema.sql create mode 100644 server/postgres/migrations/eventSchema.sql create mode 100644 server/postgres/migrations/timeSchema.sql diff --git a/models/calendar/src/index.ts b/models/calendar/src/index.ts index b2c9099059..d5ab86ce8d 100644 --- a/models/calendar/src/index.ts +++ b/models/calendar/src/index.ts @@ -68,6 +68,7 @@ export { calendarId } from '@hcengineering/calendar' export { calendarOperation } from './migration' export const DOMAIN_CALENDAR = 'calendar' as Domain +export const DOMAIN_EVENT = 'event' as Domain @Model(calendar.class.Calendar, core.class.Doc, DOMAIN_CALENDAR) @UX(calendar.string.Calendar, calendar.icon.Calendar) @@ -85,7 +86,7 @@ export class TExternalCalendar extends TCalendar implements ExternalCalendar { externalUser!: string } -@Model(calendar.class.Event, core.class.AttachedDoc, DOMAIN_CALENDAR) +@Model(calendar.class.Event, core.class.AttachedDoc, DOMAIN_EVENT) @UX(calendar.string.Event, calendar.icon.Calendar) export class TEvent extends TAttachedDoc implements Event { declare space: Ref<SystemSpace> diff --git a/models/calendar/src/migration.ts b/models/calendar/src/migration.ts index de39d23ec4..47b3d13706 100644 --- a/models/calendar/src/migration.ts +++ b/models/calendar/src/migration.ts @@ -24,7 +24,7 @@ import { type MigrationUpgradeClient } from '@hcengineering/model' import { DOMAIN_SPACE } from '@hcengineering/model-core' -import { DOMAIN_CALENDAR } from '.' +import { DOMAIN_CALENDAR, DOMAIN_EVENT } from '.' import calendar from './plugin' async function migrateCalendars (client: MigrationClient): Promise<void> { @@ -136,6 +136,16 @@ export const calendarOperation: MigrateOperation = { { state: 'migrate_calendars', func: migrateCalendars + }, + { + state: 'move-events', + func: async (client) => { + await client.move( + DOMAIN_CALENDAR, + { _class: { $in: client.hierarchy.getDescendants(calendar.class.Event) } }, + DOMAIN_EVENT + ) + } } ]) }, diff --git a/plugins/recruit-resources/src/components/review/CreateReview.svelte b/plugins/recruit-resources/src/components/review/CreateReview.svelte index b74bef5342..b00d4ab55a 100644 --- a/plugins/recruit-resources/src/components/review/CreateReview.svelte +++ b/plugins/recruit-resources/src/components/review/CreateReview.svelte @@ -13,7 +13,7 @@ // limitations under the License. --> <script lang="ts"> - import calendar from '@hcengineering/calendar' + import calendar, { Calendar } from '@hcengineering/calendar' import type { Contact, PersonAccount, Organization, Person } from '@hcengineering/contact' import contact from '@hcengineering/contact' import core, { @@ -41,7 +41,6 @@ import recruit from '../../plugin' import IconCompany from '../icons/Company.svelte' import { Analytics } from '@hcengineering/analytics' - import { getCandidateIdentifier } from '../../utils' // export let space: Ref<Project> export let candidate: Ref<Person> @@ -86,7 +85,8 @@ title, participants: [currentUser.person], eventId: '', - dueDate: 0 + dueDate: 0, + calendar: '' as Ref<Calendar> } const dispatch = createEventDispatcher() @@ -139,7 +139,7 @@ access: 'reader', allDay: false, eventId: '', - calendar: undefined + calendar: '' as Ref<Calendar> } ) diff --git a/server/postgres/migrations/calendarSchema.sql b/server/postgres/migrations/calendarSchema.sql new file mode 100644 index 0000000000..ea9a714ed5 --- /dev/null +++ b/server/postgres/migrations/calendarSchema.sql @@ -0,0 +1,5 @@ +ALTER TABLE calendar +ADD "hidden" bool; + +UPDATE calendar +SET "hidden" = (data->>'hidden')::boolean; diff --git a/server/postgres/migrations/eventSchema.sql b/server/postgres/migrations/eventSchema.sql new file mode 100644 index 0000000000..5a8c1ce9f0 --- /dev/null +++ b/server/postgres/migrations/eventSchema.sql @@ -0,0 +1,19 @@ +ALTER TABLE event +ADD "date" bigint, +ADD "dueDate" bigint, +add calendar text, +ADD "participants" text[]; + +UPDATE event +SET "date" = (data->>'date')::bigint; + +UPDATE event +SET "dueDate" = (data->>'dueDate')::bigint; + +UPDATE calendar +SET "calendar" = (data->>'calendar'); + +UPDATE event +SET "participants" = array( + SELECT jsonb_array_elements_text(data->'participants') +); diff --git a/server/postgres/migrations/timeSchema.sql b/server/postgres/migrations/timeSchema.sql new file mode 100644 index 0000000000..3a6d722293 --- /dev/null +++ b/server/postgres/migrations/timeSchema.sql @@ -0,0 +1,17 @@ +ALTER TABLE time +ADD "workslots" bigint, +ADD "doneOn" bigint, +add rank text, +ADD "user" text; + +UPDATE time +SET "workslots" = (data->>'workslots')::bigint; + +UPDATE time +SET "doneOn" = (data->>'doneOn')::bigint; + +UPDATE time +SET "rank" = (data->>'rank'); + +UPDATE time +SET "user" = (data->>'user'); diff --git a/server/postgres/src/schemas.ts b/server/postgres/src/schemas.ts index 4adb6d0c0f..0a3e482db0 100644 --- a/server/postgres/src/schemas.ts +++ b/server/postgres/src/schemas.ts @@ -155,6 +155,63 @@ const docIndexStateSchema: Schema = { } } +const timeSchema: Schema = { + ...baseSchema, + workslots: { + type: 'bigint', + notNull: false, + index: true + }, + doneOn: { + type: 'bigint', + notNull: false, + index: true + }, + user: { + type: 'text', + notNull: true, + index: true + }, + rank: { + type: 'text', + notNull: true, + index: false + } +} + +const calendarSchema: Schema = { + ...baseSchema, + hidden: { + type: 'bool', + notNull: true, + index: true + } +} + +const eventSchema: Schema = { + ...defaultSchema, + calendar: { + type: 'text', + notNull: true, + index: true + }, + date: { + type: 'bigint', + notNull: true, + index: true + }, + dueDate: { + type: 'bigint', + notNull: true, + index: true + }, + participants: { + type: 'text[]', + notNull: true, + index: true + } +} + export function addSchema (domain: string, schema: Schema): void { domainSchemas[translateDomain(domain)] = schema } @@ -166,6 +223,9 @@ export function translateDomain (domain: string): string { export const domainSchemas: Record<string, Schema> = { [DOMAIN_SPACE]: spaceSchema, [DOMAIN_TX]: txSchema, + [translateDomain('time')]: timeSchema, + [translateDomain('calendar')]: calendarSchema, + [translateDomain('event')]: eventSchema, [translateDomain(DOMAIN_DOC_INDEX_STATE)]: docIndexStateSchema, notification: notificationSchema, [translateDomain('notification-dnc')]: dncSchema, diff --git a/server/postgres/src/storage.ts b/server/postgres/src/storage.ts index 827136a4bd..412292c046 100644 --- a/server/postgres/src/storage.ts +++ b/server/postgres/src/storage.ts @@ -210,7 +210,7 @@ abstract class PostgresAdapterBase implements DbAdapter { await close(cursorName) return null } - return result.map((p) => parseDoc(p as any)) + return result.map((p) => parseDoc(p as any, _domain)) } await init() @@ -254,7 +254,7 @@ abstract class PostgresAdapterBase implements DbAdapter { } const finalSql: string = [select, ...sqlChunks].join(' ') const result = await this.client.unsafe(finalSql) - return result.map((p) => parseDocWithProjection(p as any, options?.projection)) + return result.map((p) => parseDocWithProjection(p as any, domain, options?.projection)) } buildRawOrder<T extends Doc>(domain: string, sort: SortingQuery<T>): string { @@ -307,7 +307,7 @@ abstract class PostgresAdapterBase implements DbAdapter { const res = await client.unsafe( `SELECT * FROM ${translateDomain(domain)} WHERE ${translatedQuery} FOR UPDATE` ) - const docs = res.map((p) => parseDoc(p as any)) + const docs = res.map((p) => parseDoc(p as any, domain)) for (const doc of docs) { if (doc === undefined) continue const prevAttachedTo = (doc as any).attachedTo @@ -460,11 +460,11 @@ abstract class PostgresAdapterBase implements DbAdapter { const result = await connection.unsafe(finalSql) if (options?.lookup === undefined && options?.domainLookup === undefined) { return toFindResult( - result.map((p) => parseDocWithProjection(p as any, options?.projection)), + result.map((p) => parseDocWithProjection(p as any, domain, options?.projection)), total ) } else { - const res = this.parseLookup<T>(result, joins, options?.projection) + const res = this.parseLookup<T>(result, joins, options?.projection, domain) return toFindResult(res, total) } } catch (err) { @@ -493,7 +493,8 @@ abstract class PostgresAdapterBase implements DbAdapter { private parseLookup<T extends Doc>( rows: any[], joins: JoinProps[], - projection: Projection<T> | undefined + projection: Projection<T> | undefined, + domain: string ): WithLookup<T>[] { const map = new Map<Ref<T>, WithLookup<T>>() const modelJoins: JoinProps[] = [] @@ -526,7 +527,7 @@ abstract class PostgresAdapterBase implements DbAdapter { if (res === undefined) continue const { obj, key } = res - const parsed = row[column].map(parseDoc) + const parsed = row[column].map((p: any) => parseDoc(p, domain)) obj[key] = parsed } } else if (column.startsWith('lookup_')) { @@ -982,7 +983,11 @@ abstract class PostgresAdapterBase implements DbAdapter { const val = value[operator] switch (operator) { case '$ne': - res.push(`${tkey} != '${val}'`) + if (val === null) { + res.push(`${tkey} IS NOT NULL`) + } else { + res.push(`${tkey} != '${val}'`) + } break case '$gt': res.push(`${tkey} > '${val}'`) @@ -1131,7 +1136,7 @@ abstract class PostgresAdapterBase implements DbAdapter { if (result.length === 0) { return [] } - return result.filter((it) => it != null).map((it) => parseDoc(it as any)) + return result.filter((it) => it != null).map((it) => parseDoc(it as any, domain)) } const flush = async (flush = false): Promise<void> => { @@ -1221,7 +1226,7 @@ abstract class PostgresAdapterBase implements DbAdapter { const connection = (await this.getConnection(ctx)) ?? this.client const res = await connection`SELECT * FROM ${connection(translateDomain(domain))} WHERE _id = ANY(${docs}) AND "workspaceId" = ${this.workspaceId.name}` - return res.map((p) => parseDocWithProjection(p as any)) + return res.map((p) => parseDocWithProjection(p as any, domain)) }) } @@ -1306,7 +1311,7 @@ abstract class PostgresAdapterBase implements DbAdapter { try { const res = await client`SELECT * FROM ${client(translateDomain(domain))} WHERE _id = ANY(${ids}) AND "workspaceId" = ${this.workspaceId.name} FOR UPDATE` - const docs = res.map((p) => parseDoc(p as any)) + const docs = res.map((p) => parseDoc(p as any, domain)) const map = new Map(docs.map((d) => [d._id, d])) for (const [_id, ops] of operations) { const doc = map.get(_id) @@ -1581,7 +1586,8 @@ class PostgresAdapter extends PostgresAdapterBase { forUpdate ? client` FOR UPDATE` : client`` }` const dbDoc = res[0] - return dbDoc !== undefined ? parseDoc(dbDoc as any) : undefined + const domain = this.hierarchy.getDomain(_class) + return dbDoc !== undefined ? parseDoc(dbDoc as any, domain) : undefined }) } @@ -1621,7 +1627,7 @@ class PostgresTxAdapter extends PostgresAdapterBase implements TxAdapter { const res = await this .client`SELECT * FROM ${this.client(translateDomain(DOMAIN_TX))} WHERE "workspaceId" = ${this.workspaceId.name} AND "objectSpace" = ${core.space.Model} ORDER BY _id ASC, "modifiedOn" ASC` - const model = res.map((p) => parseDoc<Tx>(p as any)) + const model = res.map((p) => parseDoc<Tx>(p as any, DOMAIN_TX)) // We need to put all core.account.System transactions first const systemTx: Tx[] = [] const userTx: Tx[] = [] diff --git a/server/postgres/src/utils.ts b/server/postgres/src/utils.ts index 77dedcad4c..9a5499d569 100644 --- a/server/postgres/src/utils.ts +++ b/server/postgres/src/utils.ts @@ -326,8 +326,8 @@ export function parseUpdate<T extends Doc> ( const remainingData: Partial<T> = {} for (const key in ops) { - if (key === '$push' || key === '$pull') { - const val = (ops as any)[key] + const val = (ops as any)[key] + if (key.startsWith('$')) { for (const k in val) { if (getDocFieldsByDomains(domain).includes(k)) { ;(extractedFields as any)[k] = val[key] @@ -337,9 +337,9 @@ export function parseUpdate<T extends Doc> ( } } else { if (getDocFieldsByDomains(domain).includes(key)) { - ;(extractedFields as any)[key] = (ops as any)[key] + ;(extractedFields as any)[key] = val } else { - ;(remainingData as any)[key] = (ops as any)[key] + ;(remainingData as any)[key] = val } } } @@ -396,8 +396,13 @@ export class DBCollectionHelper implements DomainHelperOperations { } } -export function parseDocWithProjection<T extends Doc> (doc: DBDoc, projection?: Projection<T> | undefined): T { +export function parseDocWithProjection<T extends Doc> ( + doc: DBDoc, + domain: string, + projection?: Projection<T> | undefined +): T { const { workspaceId, data, ...rest } = doc + const schema = getSchema(domain) for (const key in rest) { if ((rest as any)[key] === 'NULL' || (rest as any)[key] === null) { if (key === 'attachedTo') { @@ -407,7 +412,7 @@ export function parseDocWithProjection<T extends Doc> (doc: DBDoc, projection?: ;(rest as any)[key] = null } } - if (key === 'modifiedOn' || key === 'createdOn') { + if (schema[key] !== undefined && schema[key].type === 'bigint') { ;(rest as any)[key] = Number.parseInt((rest as any)[key]) } } @@ -427,7 +432,8 @@ export function parseDocWithProjection<T extends Doc> (doc: DBDoc, projection?: return res } -export function parseDoc<T extends Doc> (doc: DBDoc): T { +export function parseDoc<T extends Doc> (doc: DBDoc, domain: string): T { + const schema = getSchema(domain) const { workspaceId, data, ...rest } = doc for (const key in rest) { if ((rest as any)[key] === 'NULL' || (rest as any)[key] === null) { @@ -438,7 +444,7 @@ export function parseDoc<T extends Doc> (doc: DBDoc): T { ;(rest as any)[key] = null } } - if (key === 'modifiedOn' || key === 'createdOn') { + if (schema[key] !== undefined && schema[key].type === 'bigint') { ;(rest as any)[key] = Number.parseInt((rest as any)[key]) } }