From e4371c858839dbe44e43f748e2eeb09b0824862f Mon Sep 17 00:00:00 2001 From: Kallyn Gowdy Date: Thu, 14 Nov 2024 15:39:19 -0500 Subject: [PATCH] feat: Get a basic CRUD controller setup for package versions --- ...sStore.ts => MemorySubCrudRecordsStore.ts} | 75 +++++++++++-------- .../crud/sub/SubCrudRecordsController.spec.ts | 2 +- .../crud/sub/SubCrudRecordsController.ts | 9 +++ .../crud/sub/SubCrudRecordsControllerTests.ts | 36 +++++++-- .../crud/sub/SubCrudRecordsStore.ts | 27 ++++++- .../MemoryPackageVersionRecordsStore.ts | 14 +++- .../PackageVersionRecordsController.spec.ts | 40 +++++++--- .../PackageVersionRecordsController.ts | 55 ++++++++++++-- .../version/PackageVersionRecordsStore.ts | 14 ++-- 9 files changed, 204 insertions(+), 68 deletions(-) rename src/aux-records/crud/sub/{SubMemoryCrudRecordsStore.ts => MemorySubCrudRecordsStore.ts} (82%) diff --git a/src/aux-records/crud/sub/SubMemoryCrudRecordsStore.ts b/src/aux-records/crud/sub/MemorySubCrudRecordsStore.ts similarity index 82% rename from src/aux-records/crud/sub/SubMemoryCrudRecordsStore.ts rename to src/aux-records/crud/sub/MemorySubCrudRecordsStore.ts index cef460c4c..ecf03f67e 100644 --- a/src/aux-records/crud/sub/SubMemoryCrudRecordsStore.ts +++ b/src/aux-records/crud/sub/MemorySubCrudRecordsStore.ts @@ -1,6 +1,7 @@ import { SubscriptionFilter } from '../../MetricsStore'; import { MemoryStore } from '../../MemoryStore'; import { + CrudResult, GetSubCrudItemResult, ListSubCrudStoreSuccess, SubCrudRecord, @@ -36,7 +37,20 @@ export class MemorySubCrudRecordsStore< return item.key; } - async createItem(recordName: string, item: T): Promise { + async createItem(recordName: string, item: T): Promise { + const recordItem = this._itemStore.getItemByAddress( + recordName, + item.address + ); + + if (!recordItem) { + return { + success: false, + errorCode: 'parent_not_found', + errorMessage: 'The parent item was not found.', + }; + } + let bucket = this._itemBuckets.get(recordName); if (!bucket) { bucket = new Map(); @@ -53,6 +67,10 @@ export class MemorySubCrudRecordsStore< if (index < 0) { arr.push(item); } + + return { + success: true, + }; } async getItemByKey( @@ -61,30 +79,8 @@ export class MemorySubCrudRecordsStore< key: TKey ): Promise> { const bucket = this._itemBuckets.get(recordName); - if (!bucket) { - return { - item: null, - markers: [], - }; - } - - const arr = bucket.get(address); - if (!arr) { - return { - item: null, - markers: [], - }; - } - - const item = arr.find((i) => isEqual(this.getKey(i), key)) ?? null; - - if (!item) { - return { - item: null, - markers: [], - }; - } - + const arr = bucket?.get(address); + const item = arr?.find((i) => isEqual(this.getKey(i), key)) ?? null; const recordItem = await this._itemStore.getItemByAddress( recordName, address @@ -92,18 +88,25 @@ export class MemorySubCrudRecordsStore< return { item, - markers: recordItem?.markers ?? [], + markers: recordItem?.markers ?? null, }; } - async updateItem(recordName: string, item: Partial): Promise { + async updateItem( + recordName: string, + item: Partial + ): Promise { const existing = await this.getItemByKey( recordName, item.address, item as unknown as TKey ); if (!existing.item) { - return; + return { + success: false, + errorCode: 'item_not_found', + errorMessage: 'Item not found', + }; } const updated = { @@ -124,9 +127,13 @@ export class MemorySubCrudRecordsStore< } else { // Do nothing if the item does not exist. } + + return { + success: true, + }; } - async putItem(recordName: string, item: Partial): Promise { + async putItem(recordName: string, item: Partial): Promise { const existing = await this.getItemByKey( recordName, item.address, @@ -138,13 +145,17 @@ export class MemorySubCrudRecordsStore< } await this.updateItem(recordName, item); + + return { + success: true, + }; } async deleteItem( recordName: string, address: string, key: TKey - ): Promise { + ): Promise { const bucket = this._itemBuckets.get(recordName); if (!bucket) { return; @@ -159,6 +170,10 @@ export class MemorySubCrudRecordsStore< if (index >= 0) { arr.splice(index, 1); } + + return { + success: true, + }; } async listItems( diff --git a/src/aux-records/crud/sub/SubCrudRecordsController.spec.ts b/src/aux-records/crud/sub/SubCrudRecordsController.spec.ts index c22b228e4..f7d902340 100644 --- a/src/aux-records/crud/sub/SubCrudRecordsController.spec.ts +++ b/src/aux-records/crud/sub/SubCrudRecordsController.spec.ts @@ -23,7 +23,7 @@ import { CrudRecordsConfiguration, CrudRecordsController, } from '../CrudRecordsController'; -import { MemorySubCrudRecordsStore } from './SubMemoryCrudRecordsStore'; +import { MemorySubCrudRecordsStore } from './MemorySubCrudRecordsStore'; import { ActionKinds, PRIVATE_MARKER, diff --git a/src/aux-records/crud/sub/SubCrudRecordsController.ts b/src/aux-records/crud/sub/SubCrudRecordsController.ts index 066bcab4d..3d7e66f61 100644 --- a/src/aux-records/crud/sub/SubCrudRecordsController.ts +++ b/src/aux-records/crud/sub/SubCrudRecordsController.ts @@ -162,6 +162,15 @@ export abstract class SubCrudRecordsController< request.item.address, request.item.key ); + + if (!existingItem.markers) { + return { + success: false, + errorCode: 'data_not_found', + errorMessage: 'The parent item was not found.', + }; + } + const resourceMarkers = existingItem.markers; let action = existingItem.item diff --git a/src/aux-records/crud/sub/SubCrudRecordsControllerTests.ts b/src/aux-records/crud/sub/SubCrudRecordsControllerTests.ts index b18bb8c1a..aad2e2d19 100644 --- a/src/aux-records/crud/sub/SubCrudRecordsControllerTests.ts +++ b/src/aux-records/crud/sub/SubCrudRecordsControllerTests.ts @@ -266,6 +266,32 @@ export function testCrudRecordsController< }); }); + it('should return data_not_found if the record item doesnt exist', async () => { + const item = createTestItem({ + address: 'missing', + key: createKey(0), + }); + const result = (await manager.recordItem({ + recordKeyOrRecordName: recordName, + userId, + item, + instances: [], + })) as CrudRecordItemSuccess; + + expect(result).toEqual({ + success: false, + errorCode: 'data_not_found', + errorMessage: expect.any(String), + }); + + await expect( + itemsStore.getItemByKey(recordName, 'missing', createKey(0)) + ).resolves.toMatchObject({ + item: null, + markers: null, + }); + }); + it('should reject the request if given an invalid key', async () => { const result = (await manager.recordItem({ recordKeyOrRecordName: 'not_a_key', @@ -287,7 +313,7 @@ export function testCrudRecordsController< itemsStore.getItemByKey(recordName, 'address', createKey(0)) ).resolves.toMatchObject({ item: null, - markers: [], + markers: [PUBLIC_READ_MARKER], }); }); @@ -390,7 +416,7 @@ export function testCrudRecordsController< ) ).resolves.toMatchObject({ item: null, - markers: [], + markers: [PUBLIC_READ_MARKER], }); }); @@ -428,7 +454,7 @@ export function testCrudRecordsController< ) ).resolves.toMatchObject({ item: null, - markers: [], + markers: [PUBLIC_READ_MARKER], }); }); } @@ -840,7 +866,7 @@ export function testCrudRecordsController< itemsStore.getItemByKey(recordName, 'address2', createKey(2)) ).resolves.toMatchObject({ item: null, - markers: [], + markers: [PRIVATE_MARKER], }); }); @@ -898,7 +924,7 @@ export function testCrudRecordsController< ) ).resolves.toMatchObject({ item: null, - markers: [], + markers: [PRIVATE_MARKER], }); }); } else { diff --git a/src/aux-records/crud/sub/SubCrudRecordsStore.ts b/src/aux-records/crud/sub/SubCrudRecordsStore.ts index f49c370e9..8dfdb82ac 100644 --- a/src/aux-records/crud/sub/SubCrudRecordsStore.ts +++ b/src/aux-records/crud/sub/SubCrudRecordsStore.ts @@ -1,3 +1,5 @@ +import { KnownErrorCodes } from '@casual-simulation/aux-common'; + /** * Defines an interface for a store that can be used to create, read, update, and delete sub-items in a record. * That is, items which are related to a parent resource kind. (subscription to notification, package version to package, etc.) @@ -11,7 +13,7 @@ export interface SubCrudRecordsStore> { * @param recordName The name of the record. * @param item The item to create. */ - createItem(recordName: string, item: T): Promise; + createItem(recordName: string, item: T): Promise; /** * Reads the item with the given address. Always returns an object with the item and any markers that are related to the item. @@ -31,7 +33,7 @@ export interface SubCrudRecordsStore> { * @param recordName The name of the record that the item lives in. * @param record The record to update. */ - updateItem(recordName: string, item: Partial): Promise; + updateItem(recordName: string, item: Partial): Promise; /** * Creates or updates the record with the given ID. @@ -39,7 +41,7 @@ export interface SubCrudRecordsStore> { * @param recordName The name of the record that the item lives in. * @param item The item to create or update. */ - putItem(recordName: string, item: Partial): Promise; + putItem(recordName: string, item: Partial): Promise; /** * Deletes the item with the given key. @@ -47,7 +49,11 @@ export interface SubCrudRecordsStore> { * @param address The address of the record item that the item resides in. * @param key The key of the item to delete. */ - deleteItem(recordName: string, address: string, key: TKey): Promise; + deleteItem( + recordName: string, + address: string, + key: TKey + ): Promise; /** * Gets a list of the items for the given record and address. @@ -84,6 +90,7 @@ export interface GetSubCrudItemResult { /** * The markers that are related to the item. + * Null if the parent record item doesn't exist. */ markers: string[]; } @@ -100,3 +107,15 @@ export interface ListSubCrudStoreSuccess { */ totalCount: number; } + +export type CrudResult = CrudSuccess | CrudFailure; + +export interface CrudSuccess { + success: true; +} + +export interface CrudFailure { + success: false; + errorCode: 'item_not_found' | 'parent_not_found'; + errorMessage: string; +} diff --git a/src/aux-records/packages/version/MemoryPackageVersionRecordsStore.ts b/src/aux-records/packages/version/MemoryPackageVersionRecordsStore.ts index 13e2bbc22..202d81a3e 100644 --- a/src/aux-records/packages/version/MemoryPackageVersionRecordsStore.ts +++ b/src/aux-records/packages/version/MemoryPackageVersionRecordsStore.ts @@ -1,10 +1,11 @@ -import { MemoryCrudRecordsStore } from '../../crud/MemoryCrudRecordsStore'; +import { MemorySubCrudRecordsStore } from '../../crud/sub/MemorySubCrudRecordsStore'; import { PackageVersion, PackageRecordVersion, ListedPackageVersion, PackageVersionRecordsStore, PackageVersionSubscriptionMetrics, + PackageRecordVersionKey, } from './PackageVersionRecordsStore'; import { SubscriptionFilter } from '../../MetricsStore'; @@ -12,7 +13,10 @@ import { SubscriptionFilter } from '../../MetricsStore'; * A Memory-based implementation of the PackageRecordsStore. */ export class MemoryPackageVersionRecordsStore - extends MemoryCrudRecordsStore + extends MemorySubCrudRecordsStore< + PackageRecordVersionKey, + PackageRecordVersion + > implements PackageVersionRecordsStore { async getSubscriptionMetrics( @@ -30,8 +34,10 @@ export class MemoryPackageVersionRecordsStore const items = this.getItemRecord(record.name); totalItems += items.size; - for (let item of items.values()) { - totalPackageVersionBytes += item.sizeInBytes; + for (let versions of items.values()) { + for (let version of versions) { + totalPackageVersionBytes += version.sizeInBytes; + } } } diff --git a/src/aux-records/packages/version/PackageVersionRecordsController.spec.ts b/src/aux-records/packages/version/PackageVersionRecordsController.spec.ts index e33edc3f7..d893c22be 100644 --- a/src/aux-records/packages/version/PackageVersionRecordsController.spec.ts +++ b/src/aux-records/packages/version/PackageVersionRecordsController.spec.ts @@ -2,7 +2,7 @@ import { setupTestContext, TestControllers, testCrudRecordsController, -} from '../../crud/CrudRecordsControllerTests'; +} from '../../crud/sub/SubCrudRecordsControllerTests'; import { PackageVersionRecordsController } from './PackageVersionRecordsController'; import { buildSubscriptionConfig, @@ -19,35 +19,42 @@ import { import { v5 as uuidv5 } from 'uuid'; import { PackageRecordVersion, + PackageRecordVersionKey, PackageVersionRecordsStore, } from './PackageVersionRecordsStore'; import { MemoryPackageVersionRecordsStore } from './MemoryPackageVersionRecordsStore'; +import { MemoryPackageRecordsStore } from '../MemoryPackageRecordsStore'; +import { PackageRecordsStore } from '../PackageRecordsStore'; console.log = jest.fn(); console.error = jest.fn(); describe('PackageVersionRecordsController', () => { testCrudRecordsController< + PackageRecordVersionKey, PackageRecordVersion, PackageVersionRecordsStore, + PackageRecordsStore, PackageVersionRecordsController >( false, - 'package', - (services) => new MemoryPackageVersionRecordsStore(services.store), + 'package.version', + (services) => new MemoryPackageRecordsStore(services.store), + (services, packageStore) => + new MemoryPackageVersionRecordsStore(services.store, packageStore), (config, services) => new PackageVersionRecordsController({ ...config, }), + (id) => ({ + major: id, + minor: 0, + patch: 0, + tag: '', + }), (item) => ({ + key: item.key, address: item.address, - markers: item.markers, - version: { - major: 1, - minor: 0, - patch: 0, - tag: '', - }, aux: { version: 1, state: {}, @@ -60,6 +67,10 @@ describe('PackageVersionRecordsController', () => { sha256: '', sizeInBytes: 0, }), + (item) => ({ + address: item.address, + markers: item.markers, + }), async (context) => { const builder = subscriptionConfigBuilder().withUserDefaultFeatures( (features) => @@ -95,11 +106,18 @@ describe('PackageVersionRecordsController', () => { dateNowMock.mockReturnValue(999); const context = await setupTestContext< + PackageRecordVersionKey, PackageRecordVersion, PackageVersionRecordsStore, + PackageRecordsStore, PackageVersionRecordsController >( - (services) => new MemoryPackageVersionRecordsStore(services.store), + (services) => new MemoryPackageRecordsStore(services.store), + (services, packageStore) => + new MemoryPackageVersionRecordsStore( + services.store, + packageStore + ), (config, services) => { return new PackageVersionRecordsController({ ...config, diff --git a/src/aux-records/packages/version/PackageVersionRecordsController.ts b/src/aux-records/packages/version/PackageVersionRecordsController.ts index 64677e984..d22cca1e1 100644 --- a/src/aux-records/packages/version/PackageVersionRecordsController.ts +++ b/src/aux-records/packages/version/PackageVersionRecordsController.ts @@ -19,6 +19,7 @@ import { } from '../../crud'; import { PackageRecordVersion, + PackageRecordVersionKey, PackageVersionRecordsStore, PackageVersionSubscriptionMetrics, } from './PackageVersionRecordsStore'; @@ -29,6 +30,11 @@ import { PackageFeaturesConfiguration, SubscriptionConfiguration, } from '../../SubscriptionConfiguration'; +import { + SubCrudRecordsConfiguration, + SubCrudRecordsController, +} from '../../crud/sub/SubCrudRecordsController'; +import { PackageRecordsStore } from '../PackageRecordsStore'; const TRACE_NAME = 'PackageVersionRecordsController'; @@ -37,9 +43,11 @@ const TRACE_NAME = 'PackageVersionRecordsController'; */ export interface PackageVersionRecordsConfiguration extends Omit< - CrudRecordsConfiguration< + SubCrudRecordsConfiguration< + PackageRecordVersionKey, PackageRecordVersion, - PackageVersionRecordsStore + PackageVersionRecordsStore, + PackageRecordsStore >, 'resourceKind' | 'allowRecordKeys' | 'name' > {} @@ -47,9 +55,11 @@ export interface PackageVersionRecordsConfiguration /** * Defines a controller that can be used to interact with NotificationRecords. */ -export class PackageVersionRecordsController extends CrudRecordsController< +export class PackageVersionRecordsController extends SubCrudRecordsController< + PackageRecordVersionKey, PackageRecordVersion, - PackageVersionRecordsStore + PackageVersionRecordsStore, + PackageRecordsStore > { constructor(config: PackageVersionRecordsConfiguration) { super({ @@ -90,15 +100,46 @@ export class PackageVersionRecordsController extends CrudRecordsController< }; } - if (action === 'create' && typeof features.maxItems === 'number') { - if (metrics.totalItems >= features.maxItems) { + if ( + action === 'create' && + typeof features.maxPackageVersions === 'number' + ) { + if (metrics.totalItems >= features.maxPackageVersions) { + return { + success: false, + errorCode: 'subscription_limit_reached', + errorMessage: + 'The maximum number of package versions has been reached for your subscription.', + }; + } + } + + if (item && typeof features.maxPackageVersionSizeInBytes === 'number') { + if (item.sizeInBytes >= features.maxPackageVersionSizeInBytes) { return { success: false, errorCode: 'subscription_limit_reached', errorMessage: - 'The maximum number of package items has been reached for your subscription.', + 'The package version is too large for your subscription.', }; } + + if ( + action === 'create' && + typeof features.maxPackageBytesTotal === 'number' + ) { + if ( + metrics.totalPackageVersionBytes + item.sizeInBytes >= + features.maxPackageBytesTotal + ) { + return { + success: false, + errorCode: 'subscription_limit_reached', + errorMessage: + 'The maximum size of package versions has been reached for your subscription.', + }; + } + } } return { diff --git a/src/aux-records/packages/version/PackageVersionRecordsStore.ts b/src/aux-records/packages/version/PackageVersionRecordsStore.ts index f6be5cdfb..06e562163 100644 --- a/src/aux-records/packages/version/PackageVersionRecordsStore.ts +++ b/src/aux-records/packages/version/PackageVersionRecordsStore.ts @@ -9,12 +9,16 @@ import { CrudSubscriptionMetrics, } from '../../crud'; import { SubscriptionFilter } from '../../MetricsStore'; +import { + SubCrudRecord, + SubCrudRecordsStore, +} from '../../crud/sub/SubCrudRecordsStore'; /** * Defines a store that contains notification records. */ export interface PackageVersionRecordsStore - extends CrudRecordsStore { + extends SubCrudRecordsStore { /** * Gets the item metrics for the subscription of the given user or studio. * @param filter The filter to use. @@ -24,12 +28,10 @@ export interface PackageVersionRecordsStore ): Promise; } -export interface PackageRecordVersion extends CrudRecord { - /** - * The version of the package. - */ - version: PackageVersion; +export interface PackageRecordVersionKey extends PackageVersion {} +export interface PackageRecordVersion + extends SubCrudRecord { /** * The aux that is recorded in the version. */