Skip to content

Commit

Permalink
Merge pull request #862 from microsoft/feature/backing-store
Browse files Browse the repository at this point in the history
Restore backing store support
  • Loading branch information
koros authored Oct 24, 2023
2 parents a257850 + e533be5 commit 44a4071
Show file tree
Hide file tree
Showing 12 changed files with 369 additions and 9 deletions.
2 changes: 1 addition & 1 deletion packages/abstractions/src/store/backedModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ export interface BackedModel {
/**
* Gets the store that is backing the model.
*/
backingStore: BackingStore;
backingStore?: BackingStore;
}
33 changes: 33 additions & 0 deletions packages/abstractions/src/store/backedModelProxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { BackingStoreFactorySingleton } from "./backingStoreFactorySingleton";

// A method that creates a ProxyHandler for a generic model T and attaches it to a backing store.
export function createBackedModelProxyHandler<T extends {}>(): ProxyHandler<T> {

// Each model has a backing store that is created by the BackingStoreFactorySingleton
const backingStore = BackingStoreFactorySingleton.instance.createBackingStore();

/**
* The ProxyHandler for the model.
*/
const handler: ProxyHandler<T> = {
get(target, prop, receiver) {
console.debug(`BackingStore - Getting property '${prop.toString()}' from backing store`);
if (prop === 'backingStore') {
return backingStore;
}
return backingStore.get(prop.toString());
},
set(target, prop, value, receiver) {
if (prop === 'backingStore') {
console.warn(`BackingStore - Ignoring attempt to set 'backingStore' property`);
return true;
}
// set the value on the target object as well to allow it to have keys needed for serialization/deserialization
Reflect.set(target, prop, value, receiver);
console.debug(`BackingStore - Setting property '${prop.toString()}'`);
backingStore.set(prop.toString(), value);
return true;
},
};
return handler;
}
13 changes: 13 additions & 0 deletions packages/abstractions/src/store/backingStoreUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { type ParseNode } from "../serialization";

export const BackingStoreKey = "backingStoreEnabled";

/**
* Check if the object is an instance a BackedModel
* @param obj
* @returns
*/
export function isBackingStoreEnabled(fields: Record<string, (node: ParseNode) => void> ): boolean {
// Check if the fields contain the backing store key
return Object.keys(fields).includes(BackingStoreKey);
};
2 changes: 2 additions & 0 deletions packages/abstractions/src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export * from "./backingStoreParseNodeFactory";
export * from "./backingStoreSerializationWriterProxyFactory";
export * from "./inMemoryBackingStore";
export * from "./inMemoryBackingStoreFactory";
export * from "./backedModelProxy";
export * from "./backingStoreUtils";
86 changes: 86 additions & 0 deletions packages/abstractions/test/common/store/backedModelProxyTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { type BackedModel, type BackingStore, BackingStoreFactorySingleton, createBackedModelProxyHandler } from "../../../src/store";
import { assert } from "chai";

export interface Model extends BackedModel {
name?: string;
age?: number;
}

describe('createBackedModelProxyHandler', () => {
let backingStoreFactorySingleton: BackingStoreFactorySingleton;
const fakeBackingStore = {} as BackingStore;

beforeEach(() => {
backingStoreFactorySingleton = BackingStoreFactorySingleton.instance;
});

afterEach(() => {
// Reset the backing store factory if required
});

it('should get a property from the backing store', () => {
// Arrange
const handler = createBackedModelProxyHandler<Model>();
const model = new Proxy<Model>({backingStore: fakeBackingStore}, handler);

// Act
model.backingStore?.set("name", "Bob");

// Assert
assert.equal(model.backingStore?.get("name"), 'Bob');
});

it('should set a property in the backing store', () => {
// Arrange
const handler = createBackedModelProxyHandler<{name?: string}>();
const model = new Proxy<Model>({backingStore: fakeBackingStore}, handler);

// Act
model.name = 'Bob';

// Assert
assert.equal(model.backingStore?.get("name"), 'Bob');
});

it('should get and set multiple properties in the backing store', () => {
// Arrange
const handler = createBackedModelProxyHandler();
const model = new Proxy<Model>({backingStore: fakeBackingStore}, handler);

// Act
model.name = 'Bob';
model.age = 30;
const name = model.name;
const age = model.age;

// Assert
assert.equal(model.backingStore?.get("name"), name);
assert.equal(model.backingStore?.get("age"), age);
});

it('should ignore setting the backingStore property', () => {
// Arrange
const handler = createBackedModelProxyHandler();
const model = new Proxy<Model>({backingStore: fakeBackingStore}, handler);

// Act
const dummyBackingStore = {} as BackingStore;
model.backingStore = dummyBackingStore;

// Assert
assert.notEqual(model.backingStore, dummyBackingStore);
});

it('should return the backing store when the property itself is backingStore', () => {
// Arrange
const handler = createBackedModelProxyHandler();
const model = new Proxy<Model>({backingStore: fakeBackingStore}, handler);

// Act
const backingStore = model.backingStore;

// Assert
assert.isDefined(model.backingStore);
assert.notEqual(model.backingStore, fakeBackingStore);
});
});
18 changes: 18 additions & 0 deletions packages/abstractions/test/common/store/backingStoreUtilsTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { assert } from "chai";
import { isBackingStoreEnabled } from "../../../src/store/backingStoreUtils";
import { type TestBackedModel, createTestBackedModelFromDiscriminatorValue, createTestParserFromDiscriminatorValue } from "./testEntity";
import { type ParseNode } from "../../../src";

it("Test backing store should be enabled if the parsableFactory has backingStore property", async () => {
const testBackedModel = {} as TestBackedModel;
const fields = createTestBackedModelFromDiscriminatorValue({} as ParseNode)(testBackedModel);
const backingStoreEnabled = isBackingStoreEnabled(fields);
assert.isTrue(backingStoreEnabled);
});

it("Test backing store should not be enabled if the parsableFactory lacks backingStore property", async () => {
const testModel = {} as TestBackedModel;
const fields = createTestParserFromDiscriminatorValue({} as ParseNode)(testModel);
const backingStoreEnabled = isBackingStoreEnabled(fields);
assert.isFalse(backingStoreEnabled);
});
100 changes: 100 additions & 0 deletions packages/abstractions/test/common/store/testEntity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { BackedModel, BackingStore, Parsable, ParseNode } from "../../../src";

const fakeBackingStore: BackingStore = {} as BackingStore;

export interface TestParser {
testString?: string | undefined;
foos?: FooResponse[] | undefined;
}
export interface TestBackedModel extends TestParser, BackedModel {
backingStoreEnabled?: boolean | undefined;
}
export interface FooResponse extends Parsable {
id?: string | undefined;
bars?: BarResponse[] | undefined;
}
export interface BarResponse extends Parsable {
propA?: string | undefined;
propB?: string | undefined;
propC?: Date | undefined;
}

export function createTestParserFromDiscriminatorValue(
parseNode: ParseNode | undefined
) {
if (!parseNode) throw new Error("parseNode cannot be undefined");
return deserializeTestParser;
}

export function createTestBackedModelFromDiscriminatorValue(
parseNode: ParseNode | undefined
) {
if (!parseNode) throw new Error("parseNode cannot be undefined");
return deserializeTestBackedModel;
}

export function createFooParserFromDiscriminatorValue(
parseNode: ParseNode | undefined
) {
if (!parseNode) throw new Error("parseNode cannot be undefined");
return deserializeFooParser;
}

export function createBarParserFromDiscriminatorValue(
parseNode: ParseNode | undefined
) {
if (!parseNode) throw new Error("parseNode cannot be undefined");
return deserializeBarParser;
}

export function deserializeTestParser(
testParser: TestParser | undefined = {}
): Record<string, (node: ParseNode) => void> {
return {
foos: (n) => {
testParser.foos = n.getCollectionOfObjectValues(createFooParserFromDiscriminatorValue);
}
};
}

export function deserializeTestBackedModel(
testParser: TestBackedModel | undefined = {}
): Record<string, (node: ParseNode) => void> {
return {
backingStoreEnabled: (n) => {
testParser.backingStoreEnabled = true;
},
foos: (n) => {
testParser.foos = n.getCollectionOfObjectValues(createFooParserFromDiscriminatorValue);
}
};
}

export function deserializeFooParser(
fooResponse: FooResponse | undefined = {}
): Record<string, (node: ParseNode) => void> {
return {
id: (n) => {
fooResponse.id = n.getStringValue();
},
bars: (n) => {
fooResponse.bars = n.getCollectionOfObjectValues(createBarParserFromDiscriminatorValue);
}
};
}

export function deserializeBarParser(
barResponse: BarResponse | undefined = {}
): Record<string, (node: ParseNode) => void> {
return {
propA: (n) => {
barResponse.propA = n.getStringValue();
},
propB: (n) => {
barResponse.propB = n.getStringValue();
},
propC: (n) => {
barResponse.propC = n.getDateValue();
}
};
}
7 changes: 6 additions & 1 deletion packages/serialization/form/src/formParseNode.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import {
BackedModel,
createBackedModelProxyHandler,
DateOnly,
Duration,
type Parsable,
type ParsableFactory,
parseGuidString,
type ParseNode,
TimeOnly,
isBackingStoreEnabled,
toFirstCharacterUpper,
} from "@microsoft/kiota-abstractions";

Expand Down Expand Up @@ -78,7 +81,9 @@ export class FormParseNode implements ParseNode {
public getObjectValue = <T extends Parsable>(
parsableFactory: ParsableFactory<T>,
): T => {
const value: T = {} as T;
const temp: T = {} as T;
const enableBackingStore = isBackingStoreEnabled(parsableFactory(this)(temp));
const value: T = enableBackingStore ? new Proxy(temp, createBackedModelProxyHandler<T>()) : temp;
if (this.onBeforeAssignFieldValues) {
this.onBeforeAssignFieldValues(value);
}
Expand Down
8 changes: 5 additions & 3 deletions packages/serialization/json/src/jsonParseNode.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {
createBackedModelProxyHandler,
DateOnly,
Duration,
type Parsable,
type ParsableFactory,
parseGuidString,
type ParseNode,
TimeOnly,
isBackingStoreEnabled,
toFirstCharacterUpper,
} from "@microsoft/kiota-abstractions";

Expand Down Expand Up @@ -69,7 +71,9 @@ export class JsonParseNode implements ParseNode {
public getObjectValue = <T extends Parsable>(
parsableFactory: ParsableFactory<T>,
): T => {
const value: T = {} as T;
const temp: T = {} as T;
const enableBackingStore = isBackingStoreEnabled(parsableFactory(this)(temp));
const value: T = enableBackingStore ? new Proxy(temp, createBackedModelProxyHandler<T>()) : temp;
if (this.onBeforeAssignFieldValues) {
this.onBeforeAssignFieldValues(value);
}
Expand All @@ -85,11 +89,9 @@ export class JsonParseNode implements ParseNode {
parsableFactory: ParsableFactory<T>,
): void => {
const fields = parsableFactory(this)(model);

if (!this._jsonNode) return;
Object.entries(this._jsonNode as any).forEach(([k, v]) => {
const deserializer = fields[k];

if (deserializer) {
deserializer(new JsonParseNode(v));
} else {
Expand Down
Loading

0 comments on commit 44a4071

Please sign in to comment.