diff --git a/api/constants.ts b/api/constants.ts index fee88cdd..c42fc517 100644 --- a/api/constants.ts +++ b/api/constants.ts @@ -11,6 +11,7 @@ export { AJV_PATTERN_KEYS_NOT_OBJECT, US_ERRORS, CURRENT_TIMESTAMP, + OrderBy, DEFAULT_QUERY_TIMEOUT, EXTENDED_QUERY_TIMEOUT, ALL_COLUMNS, diff --git a/api/db.ts b/api/db.ts index 46c8797e..f352e2aa 100644 --- a/api/db.ts +++ b/api/db.ts @@ -58,3 +58,17 @@ export { JoinedEntryRevisionTenant, JoinedEntryRevisionTenantColumns, } from '../src/db/presentations/joined-entry-revision-tenant'; + +export { + LicenseLimit, + LicenseLimitColumn, + LicenseLimitColumnRaw, +} from '../src/db/models/new/license-limit'; +export {LicenseLimitWithStartedInFuture} from '../src/db/models/new/license-limit/presentations'; +export { + LicenseAssignment, + LicenseAssignmentColumnRaw, + LicenseAssignmentColumn, +} from '../src/db/models/new/license-assignment'; +export {LicenseType} from '../src/db/models/new/license-assignment/types'; +export {LicenseAssignmentWithIsActive} from '../src/db/models/new/license-assignment/presentations'; diff --git a/api/utils.ts b/api/utils.ts index 9769ed6a..3f258caf 100644 --- a/api/utils.ts +++ b/api/utils.ts @@ -4,6 +4,7 @@ export { isTenantIdWithOrgId, getOrgIdFromTenantId, makeTenantIdFromOrgId, + mapValuesToSnakeCase, } from '../src/utils'; export {normalizedEnv} from '../src/utils/normalized-env'; diff --git a/src/const/common.ts b/src/const/common.ts index bbaad4f1..8a10b875 100644 --- a/src/const/common.ts +++ b/src/const/common.ts @@ -204,6 +204,10 @@ export const DEFAULT_PAGE_SIZE = 1000; export const DEFAULT_PAGE = 0; export const CURRENT_TIMESTAMP = 'CURRENT_TIMESTAMP'; +export enum OrderBy { + Asc = 'ASC', + Desc = 'DESC', +} export const APP_NAME = 'united-storage'; diff --git a/src/db/migrations/20251019110620_add_table_license_limits.ts b/src/db/migrations/20251019110620_add_table_license_limits.ts new file mode 100644 index 00000000..d88c0675 --- /dev/null +++ b/src/db/migrations/20251019110620_add_table_license_limits.ts @@ -0,0 +1,52 @@ +import type {Knex} from 'knex'; + +export async function up(knex: Knex): Promise { + return knex.raw(` + CREATE TABLE license_limits ( + license_limit_id BIGINT NOT NULL PRIMARY KEY DEFAULT get_id(), + tenant_id TEXT NOT NULL DEFAULT 'common' REFERENCES tenants (tenant_id) ON UPDATE CASCADE ON DELETE CASCADE, + limit_value INT NOT NULL, + started_at TIMESTAMPTZ NOT NULL, + created_by TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_by TEXT NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ); + + CREATE INDEX license_limits_tenant_id_started_at_idx ON license_limits(tenant_id, started_at); + + CREATE TYPE LICENSE_TYPE AS ENUM ('creator', 'visitor'); + + CREATE TABLE license_assignments ( + license_assignment_id BIGINT NOT NULL PRIMARY KEY DEFAULT get_id(), + tenant_id TEXT NOT NULL DEFAULT 'common' REFERENCES tenants (tenant_id) ON UPDATE CASCADE ON DELETE CASCADE, + user_id TEXT NOT NULL, + license_type LICENSE_TYPE NOT NULL, + expires_at TIMESTAMPTZ, + created_by TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_by TEXT NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ); + + CREATE UNIQUE INDEX license_assignments_tenant_id_user_id_idx ON license_assignments(tenant_id, user_id); + CREATE INDEX license_assignments_expires_at_idx ON license_assignments(expires_at); + CREATE INDEX license_assignments_created_at_idx ON license_assignments(created_at); + CREATE INDEX license_assignments_updated_at_idx ON license_assignments(updated_at); + + `); +} + +export async function down(knex: Knex): Promise { + return knex.raw(` + DROP INDEX license_assignments_updated_at_idx; + DROP INDEX license_assignments_created_at_idx; + DROP INDEX license_assignments_expires_at_idx; + DROP INDEX license_assignments_tenant_id_user_id_idx; + DROP TABLE license_assignments; + DROP TYPE LICENSE_TYPE; + + DROP INDEX license_limits_tenant_id_started_at_idx; + DROP TABLE license_limits; + `); +} diff --git a/src/db/models/new/license-assignment/index.ts b/src/db/models/new/license-assignment/index.ts new file mode 100644 index 00000000..c8b646a6 --- /dev/null +++ b/src/db/models/new/license-assignment/index.ts @@ -0,0 +1,38 @@ +import {Model} from '../../..'; +import {mapValuesToSnakeCase} from '../../../../utils'; + +import {LicenseType} from './types'; + +export const LicenseAssignmentColumn = { + LicenseAssignmentId: 'licenseAssignmentId', + TenantId: 'tenantId', + UserId: 'userId', + LicenseType: 'licenseType', + ExpiresAt: 'expiresAt', + CreatedBy: 'createdBy', + CreatedAt: 'createdAt', + UpdatedBy: 'updatedBy', + UpdatedAt: 'updatedAt', +} as const; + +export const LicenseAssignmentColumnRaw = mapValuesToSnakeCase(LicenseAssignmentColumn); + +export class LicenseAssignment extends Model { + static get tableName() { + return 'license_assignments'; + } + + static get idColumn() { + return LicenseAssignmentColumn.LicenseAssignmentId; + } + + [LicenseAssignmentColumn.LicenseAssignmentId]!: string; + [LicenseAssignmentColumn.TenantId]!: string; + [LicenseAssignmentColumn.UserId]!: string; + [LicenseAssignmentColumn.LicenseType]!: `${LicenseType}`; + [LicenseAssignmentColumn.ExpiresAt]!: Nullable; + [LicenseAssignmentColumn.CreatedBy]!: string; + [LicenseAssignmentColumn.CreatedAt]!: string; + [LicenseAssignmentColumn.UpdatedBy]!: string; + [LicenseAssignmentColumn.UpdatedAt]!: string; +} diff --git a/src/db/models/new/license-assignment/presentations/index.ts b/src/db/models/new/license-assignment/presentations/index.ts new file mode 100644 index 00000000..1c0f0300 --- /dev/null +++ b/src/db/models/new/license-assignment/presentations/index.ts @@ -0,0 +1 @@ +export * from './license-assignment-with-is-active'; diff --git a/src/db/models/new/license-assignment/presentations/license-assignment-with-is-active.ts b/src/db/models/new/license-assignment/presentations/license-assignment-with-is-active.ts new file mode 100644 index 00000000..573c7735 --- /dev/null +++ b/src/db/models/new/license-assignment/presentations/license-assignment-with-is-active.ts @@ -0,0 +1,27 @@ +import {QueryBuilder, TransactionOrKnex, raw} from 'objection'; + +import {CURRENT_TIMESTAMP} from '../../../../../const'; +import {LicenseAssignment, LicenseAssignmentColumnRaw} from '../index'; + +export class LicenseAssignmentWithIsActive extends LicenseAssignment { + protected static get selectedColumns() { + return [ + '*', + raw(`?? > ${CURRENT_TIMESTAMP} OR ?? IS NULL`, [ + LicenseAssignmentColumnRaw.ExpiresAt, + LicenseAssignmentColumnRaw.ExpiresAt, + ]).as('is_active'), + ]; + } + + static getSelectQuery(trx: TransactionOrKnex) { + const query = LicenseAssignment.query(trx).select(this.selectedColumns); + + return query as QueryBuilder< + LicenseAssignmentWithIsActive, + LicenseAssignmentWithIsActive[] + >; + } + + isActive!: boolean; +} diff --git a/src/db/models/new/license-assignment/types.ts b/src/db/models/new/license-assignment/types.ts new file mode 100644 index 00000000..7377c504 --- /dev/null +++ b/src/db/models/new/license-assignment/types.ts @@ -0,0 +1,4 @@ +export enum LicenseType { + Creator = 'creator', + Visitor = 'visitor', +} diff --git a/src/db/models/new/license-limit/index.ts b/src/db/models/new/license-limit/index.ts new file mode 100644 index 00000000..ca49fc98 --- /dev/null +++ b/src/db/models/new/license-limit/index.ts @@ -0,0 +1,34 @@ +import {Model} from '../../..'; +import {mapValuesToSnakeCase} from '../../../../utils'; + +export const LicenseLimitColumn = { + LicenseLimitId: 'licenseLimitId', + TenantId: 'tenantId', + LimitValue: 'limitValue', + StartedAt: 'startedAt', + CreatedBy: 'createdBy', + CreatedAt: 'createdAt', + UpdatedBy: 'updatedBy', + UpdatedAt: 'updatedAt', +} as const; + +export const LicenseLimitColumnRaw = mapValuesToSnakeCase(LicenseLimitColumn); + +export class LicenseLimit extends Model { + static get tableName() { + return 'license_limits'; + } + + static get idColumn() { + return LicenseLimitColumn.LicenseLimitId; + } + + [LicenseLimitColumn.LicenseLimitId]!: string; + [LicenseLimitColumn.TenantId]!: string; + [LicenseLimitColumn.LimitValue]!: number; + [LicenseLimitColumn.StartedAt]!: string; + [LicenseLimitColumn.CreatedBy]!: string; + [LicenseLimitColumn.CreatedAt]!: string; + [LicenseLimitColumn.UpdatedBy]!: string; + [LicenseLimitColumn.UpdatedAt]!: string; +} diff --git a/src/db/models/new/license-limit/presentations/index.ts b/src/db/models/new/license-limit/presentations/index.ts new file mode 100644 index 00000000..5785af85 --- /dev/null +++ b/src/db/models/new/license-limit/presentations/index.ts @@ -0,0 +1 @@ +export * from './license-limit-with-started-in-future'; diff --git a/src/db/models/new/license-limit/presentations/license-limit-with-started-in-future.ts b/src/db/models/new/license-limit/presentations/license-limit-with-started-in-future.ts new file mode 100644 index 00000000..c4db9222 --- /dev/null +++ b/src/db/models/new/license-limit/presentations/license-limit-with-started-in-future.ts @@ -0,0 +1,26 @@ +import {QueryBuilder, TransactionOrKnex, raw} from 'objection'; + +import {CURRENT_TIMESTAMP} from '../../../../../const'; +import {LicenseLimit, LicenseLimitColumnRaw} from '../index'; + +export class LicenseLimitWithStartedInFuture extends LicenseLimit { + protected static get selectedColumns() { + return [ + '*', + raw(`?? > ${CURRENT_TIMESTAMP}`, [LicenseLimitColumnRaw.StartedAt]).as( + 'started_in_future', + ), + ]; + } + + static getSelectQuery(trx: TransactionOrKnex) { + const query = LicenseLimit.query(trx).select(this.selectedColumns); + + return query as QueryBuilder< + LicenseLimitWithStartedInFuture, + LicenseLimitWithStartedInFuture[] + >; + } + + startedInFuture!: boolean; +} diff --git a/src/utils/cases.ts b/src/utils/cases.ts new file mode 100644 index 00000000..d3b1b9cc --- /dev/null +++ b/src/utils/cases.ts @@ -0,0 +1,18 @@ +type SnakeCase = S extends `${infer T}${infer U}` + ? `${T extends Capitalize ? '_' : ''}${Lowercase}${SnakeCase}` + : S; + +type MapToSnakeCase> = { + [K in keyof T]: SnakeCase; +}; + +const toSnakeCase = (str: string): string => + str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); + +export const mapValuesToSnakeCase = >( + obj: T, +): MapToSnakeCase => { + return Object.fromEntries( + Object.entries(obj).map(([key, value]) => [key, toSnakeCase(value)]), + ) as MapToSnakeCase; +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index 383235e1..78688e91 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -5,3 +5,4 @@ export * from './user'; export * from './validation'; export * from './tenant'; export * from './promise'; +export * from './cases';