Skip to content

Commit

Permalink
feat(core): add user
Browse files Browse the repository at this point in the history
  • Loading branch information
cstrnt committed Jan 30, 2025
1 parent f018ef1 commit 79eef4b
Show file tree
Hide file tree
Showing 14 changed files with 328 additions and 14 deletions.
5 changes: 5 additions & 0 deletions apps/web/abby.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineConfig } from "@tryabby/core";
import * as validation from "@tryabby/core/validation";

export default defineConfig(
{
Expand All @@ -24,5 +25,9 @@ export default defineConfig(
abc: "JSON",
},
cookies: { disableByDefault: true, expiresInDays: 30 },
user: {
id: validation.string(),
email: validation.string(),
},
}
);
5 changes: 5 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
"require": "./dist/defineConfig/index.js",
"import": "./dist/defineConfig/index.js",
"types": "./dist/defineConfig/index.d.ts"
},
"./validation": {
"require": "./dist/validation/index.js",
"import": "./dist/validation/index.js",
"types": "./dist/validation/index.d.ts"
}
},
"scripts": {
Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/defineConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ABConfig, AbbyConfig, RemoteConfigValueString } from ".";
import type { ValidatorType } from "./validation";

export const DYNAMIC_ABBY_CONFIG_KEYS = [
"projectId",
Expand All @@ -15,13 +16,17 @@ export function defineConfig<
const Tests extends Record<string, ABConfig>,
const RemoteConfig extends Record<RemoteConfigName, RemoteConfigValueString>,
const RemoteConfigName extends Extract<keyof RemoteConfig, string>,
const User extends Record<string, ValidatorType> = Record<
string,
ValidatorType
>,
>(
dynamicConfig: Pick<
AbbyConfig<FlagName, Tests, string[], RemoteConfigName, RemoteConfig>,
AbbyConfig<FlagName, Tests, string[], RemoteConfigName, RemoteConfig, User>,
DynamicConfigKeys
>,
config: Omit<
AbbyConfig<FlagName, Tests, string[], RemoteConfigName, RemoteConfig>,
AbbyConfig<FlagName, Tests, string[], RemoteConfigName, RemoteConfig, User>,
DynamicConfigKeys
>
) {
Expand Down
22 changes: 21 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
remoteConfigStringToType,
stringifyRemoteConfigValue,
} from "./shared/";
import type { ValidatorType, Infer } from "./validation";

export * from "./shared/index";
export {
Expand Down Expand Up @@ -93,6 +94,7 @@ export type AbbyConfig<
RemoteConfigName,
RemoteConfigValueString
> = Record<RemoteConfigName, RemoteConfigValueString>,
User extends Record<string, ValidatorType> = Record<string, ValidatorType>,
> = {
projectId: string;
apiUrl?: string;
Expand All @@ -109,6 +111,7 @@ export type AbbyConfig<
expiresInDays?: number;
};
__experimentalCdnUrl?: string;
user?: User;
};

export class Abby<
Expand All @@ -118,6 +121,10 @@ export class Abby<
const RemoteConfig extends Record<RemoteConfigName, RemoteConfigValueString>,
const RemoteConfigName extends Extract<keyof RemoteConfig, string>,
const Environments extends Array<string> = Array<string>,
const User extends Record<string, ValidatorType> = Record<
string,
ValidatorType
>,
> {
private log = (...args: any[]) =>
this.config.debug ? console.log("core.Abby", ...args) : () => {};
Expand All @@ -144,14 +151,16 @@ export class Abby<
private remoteConfigOverrides = new Map<string, RemoteConfigValue>();

private COOKIE_CONSENT_KEY = "$_abcc_$";
private user = {} as User;

constructor(
private config: AbbyConfig<
FlagName,
Tests,
Environments,
RemoteConfigName,
RemoteConfig
RemoteConfig,
User
>,
private persistantTestStorage?: PersistentStorage,
private persistantFlagStorage?: PersistentStorage,
Expand Down Expand Up @@ -725,4 +734,15 @@ export class Abby<
);
});
}

updateUserProperties(
user: Partial<{
-readonly [K in keyof User]: Infer<User[K]>;
}>
) {
this.user = {
...this.user,
...user,
};
}
}
12 changes: 12 additions & 0 deletions packages/core/src/shared/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ export const remoteConfigValueStringSchema = z.union([
z.literal("JSON"),
]);

const validatorType = z.object({
type: z.union([
z.literal("string"),
z.literal("number"),
z.literal("boolean"),
]),
optional: z.boolean().optional(),
});

export type ValidatorType = z.infer<typeof validatorType>;

export const abbyConfigSchema = z.object({
projectId: z.string(),
apiUrl: z.string().optional(),
Expand Down Expand Up @@ -67,6 +78,7 @@ export const abbyConfigSchema = z.object({
})
.optional(),
__experimentalCdnUrl: z.string().optional(),
user: z.record(z.string(), validatorType).optional(),
}) satisfies z.ZodType<AbbyConfig>;

export type AbbyConfigFile = z.infer<typeof abbyConfigSchema>;
Expand Down
89 changes: 89 additions & 0 deletions packages/core/src/validation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import type { ValidatorType } from "../shared/schemas";
export type { ValidatorType };

type StringValidatorType = { type: "string" };
type NumberValidatorType = { type: "number" };
type BooleanValidatorType = { type: "boolean" };

export const string = () => ({ type: "string" }) as StringValidatorType;
export const number = () => ({ type: "number" }) as NumberValidatorType;
export const boolean = () => ({ type: "boolean" }) as BooleanValidatorType;

export const optional = <
T extends StringValidatorType | NumberValidatorType | BooleanValidatorType,
>(
type: T
) => ({ ...type, optional: true }) as const;

export type Infer<T> = T extends StringValidatorType
? T extends { optional: true }
? string | null | undefined
: string
: T extends NumberValidatorType
? T extends { optional: true }
? number | null | undefined
: number
: T extends BooleanValidatorType
? T extends { optional: true }
? boolean | null | undefined
: boolean
: never;

type Errors = Array<{
property: string;
message: string;
}>;

export function validate<T extends Record<string, ValidatorType>>(
userValidator: T,
user: Record<string, unknown>
):
| {
errors: Errors;
value?: never;
}
| {
errors?: never;
value: {
[key in keyof T]: Infer<T[keyof T]>;
};
} {
const returnObject: {
[key in keyof T]: Infer<T[keyof T]>;
} = {} as any;
const errors: Errors = [];
for (const key in userValidator) {
const validator = userValidator[key];
const value = user[key];

if (value === undefined && "optional" in validator) {
continue;
}

if (validator.type === "string" && typeof value !== "string") {
errors.push({
property: key,
message: `Expected string but got ${typeof value}`,
});
}

if (validator.type === "number" && typeof value !== "number") {
errors.push({
property: key,
message: `Expected number but got ${typeof value}`,
});
}

if (validator.type === "boolean" && typeof value !== "boolean") {
errors.push({
property: key,
message: `Expected boolean but got ${typeof value}`,
});
}
returnObject[key] = value as Infer<typeof validator>;
}
if (errors.length > 0) {
return { errors };
}
return { value: returnObject };
}
11 changes: 11 additions & 0 deletions packages/core/tests/defineConfig.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Abby } from "../src";
import { defineConfig } from "../src/defineConfig";
import * as validation from "../src/validation";

describe("defineConfig", () => {
const cfg = defineConfig(
Expand All @@ -17,6 +18,11 @@ describe("defineConfig", () => {
variants: ["true", "false"],
},
},
user: {
name: validation.string(),
age: validation.number(),
isDeveloper: validation.boolean(),
},
}
);

Expand All @@ -32,5 +38,10 @@ describe("defineConfig", () => {
>();

expectTypeOf(abby.getVariants).parameter(0).toEqualTypeOf<"abTest">();
expectTypeOf(abby.updateUserProperties).parameter(0).toEqualTypeOf<{
name?: string;
age?: number;
isDeveloper?: boolean;
}>();
});
});
20 changes: 20 additions & 0 deletions packages/core/tests/types.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { RemoteConfigValueStringToType } from "../dist";
import { Abby, type AbbyConfig } from "../src/index";
import * as validation from "../src/validation";

describe("types", () => {
it("produces proper types", () => {
Expand Down Expand Up @@ -45,6 +46,25 @@ describe("types", () => {
});
});

describe("user types", () => {
const abby = new Abby({
environments: [""],
currentEnvironment: "",
projectId: "",
user: {
name: validation.string(),
age: validation.number(),
isDeveloper: validation.boolean(),
},
});

expectTypeOf(abby.updateUserProperties).parameter(0).toEqualTypeOf<{
name?: string;
age?: number;
isDeveloper?: boolean;
}>();
});

describe("Type Helpers", () => {
it("converts the strings properly", () => {
expectTypeOf<
Expand Down
90 changes: 90 additions & 0 deletions packages/core/tests/validation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import * as validation from "../src/validation";

describe("validation", () => {
it("produces proper types", () => {
const booleanValidation = validation.boolean();
expectTypeOf<
validation.Infer<typeof booleanValidation>
>().toEqualTypeOf<boolean>();

const stringValidation = validation.string();
expectTypeOf<
validation.Infer<typeof stringValidation>
>().toEqualTypeOf<string>();

const numberValidation = validation.number();
expectTypeOf<
validation.Infer<typeof numberValidation>
>().toEqualTypeOf<number>();

const optionalNumberValidation = validation.optional(validation.number());

expectTypeOf<
validation.Infer<typeof optionalNumberValidation>
>().toEqualTypeOf<number | undefined | null>();

const optionalStringValidation = validation.optional(validation.string());

expectTypeOf<
validation.Infer<typeof optionalStringValidation>
>().toEqualTypeOf<string | undefined | null>();

const optionalBooleanValidation = validation.optional(validation.boolean());

expectTypeOf<
validation.Infer<typeof optionalBooleanValidation>
>().toEqualTypeOf<boolean | undefined | null>();
});

it("validates properly", () => {
const userValidator = {
name: validation.string(),
age: validation.number(),
isDeveloper: validation.boolean(),
};
const user = {
name: "John",
age: 25,
isDeveloper: true,
};
expect(validation.validate(userValidator, user)).toEqual({
value: user,
});
});

it("validates properly with optional fields", () => {
const userValidator = {
name: validation.string(),
age: validation.optional(validation.number()),
isDeveloper: validation.boolean(),
};
const user = {
name: "John",
isDeveloper: true,
};
expect(validation.validate(userValidator, user)).toEqual({
value: user,
});
});

it("shows errors properly", () => {
const userValidator = {
name: validation.string(),
age: validation.number(),
isDeveloper: validation.boolean(),
};
const user = {
name: "John",
age: "25",
isDeveloper: true,
};
expect(validation.validate(userValidator, user)).toEqual({
errors: [
{
property: "age",
message: "Expected number but got string",
},
],
});
});
});
Loading

0 comments on commit 79eef4b

Please sign in to comment.