Skip to content

Commit

Permalink
feat(sdk): from_random injection (#593)
Browse files Browse the repository at this point in the history
<!--
Pull requests are squash merged using:
- their title as the commit message
- their description as the commit body

Having a good title and description is important for the users to get
readable changelog and understand when they need to update his code and
how.
-->

<!-- Explain WHAT the change is -->
This change includes changes in StringFormats(added some string
formats), logic to provide random values for type nodes and tests to
validate the changes.
The changes are mostly in the typegraph sdk. 

#### Motivation and context

<!-- Explain WHY the was made or link an issue number -->
This feature enables the user to inject random values for a field(**Type
Node**) when defining a **Typegraph**.

#### Migration notes
_No changes needed_.

<!-- Explain HOW users should update their code when required -->

### Checklist

- [x] The change come with new or modified tests
- [x] Hard-to-understand functions have explanatory comments
- [ ] End-user documentation is updated to reflect the change
  • Loading branch information
destifo authored Feb 21, 2024
1 parent 961fab5 commit bbf0b95
Show file tree
Hide file tree
Showing 28 changed files with 461 additions and 53 deletions.
1 change: 1 addition & 0 deletions libs/common/src/typegraph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ pub struct TypeMeta {
pub auths: Vec<Auth>,
pub rate: Option<Rate>,
pub version: String,
pub random_seed: Option<u32>,
}

#[cfg_attr(feature = "codegen", derive(JsonSchema))]
Expand Down
2 changes: 1 addition & 1 deletion libs/common/src/typegraph/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub enum Injection {
Secret(InjectionData<String>),
Parent(InjectionData<u32>),
Dynamic(InjectionData<String>),
Random(InjectionData<String>),
}

#[cfg_attr(feature = "codegen", derive(JsonSchema))]
Expand Down Expand Up @@ -116,7 +117,6 @@ pub enum StringFormat {
Ean,
Date,
DateTime,
// Path,
Phone,
}

Expand Down
8 changes: 8 additions & 0 deletions typegate/deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion typegate/src/engine/planner/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ class ArgumentCollector {
private collectArgImpl(node: CollectNode): ComputeArg {
const { astNode, typeIdx } = node;

const typ = this.tg.type(typeIdx);
const typ: TypeNode = this.tg.type(typeIdx);

this.addPoliciesFrom(typeIdx);

Expand Down Expand Up @@ -695,6 +695,10 @@ class ArgumentCollector {
}
return generator;
}

case "random": {
return () => this.tg.getRandom(typ);
}
}
}

Expand Down
130 changes: 94 additions & 36 deletions typegate/src/runtimes/random.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,48 +78,106 @@ export class RandomRuntime extends Runtime {

execute(typ: TypeNode): Resolver {
return () => {
return this.randomizeRecursively(typ);
return randomizeRecursively(typ, this.chance, this._tgTypes);
};
}
}

randomizeRecursively(typ: TypeNode): any {
const config = typ.config ?? {};
if (Object.prototype.hasOwnProperty.call(config, "gen")) {
const { gen, ...arg } = config;
return this.chance[gen as string](arg);
export default function randomizeRecursively(
typ: TypeNode,
chance: typeof Chance,
tgTypes: TypeNode[],
): any {
const config = typ.config ?? {};
if (Object.prototype.hasOwnProperty.call(config, "gen")) {
const { gen, ...arg } = config;
return chance[gen as string](arg);
}
switch (typ.type) {
case "object":
return {};
case "optional": {
const childNodeName = tgTypes[typ.item];
return chance.bool()
? randomizeRecursively(childNodeName, chance, tgTypes)
: null;
}
switch (typ.type) {
case "object":
return {};
case "optional": {
const childNodeName = this.getTgTypeNameByIndex(typ.item);
return this.chance.bool()
? this.randomizeRecursively(childNodeName)
: null;
case "integer":
return chance.integer();
case "string":
if (typ.format === "uuid") {
return chance.guid();
}
if (typ.format === "email") {
return chance.email();
}
if (typ.format === "uri") {
return chance.url();
}
if (typ.format === "hostname") {
return chance.domain();
}
if (typ.format === "date-time") {
const randomDate = chance.date();

// Get the timestamp of the random date
const timestamp = randomDate.getTime();
console.log(randomDate);

// Create a new Date object with the timestamp adjusted for the local timezone offset
const dateInUtc = new Date(
timestamp - randomDate.getTimezoneOffset() * 60000,
);
return dateInUtc.toISOString();
}
if (typ.format === "phone") {
return chance.phone();
}
case "integer":
return this.chance.integer();
case "string":
if (typ.format === "uuid") {
return this.chance.guid();
}
if (typ.format === "email") {
return this.chance.email();
}
return this.chance.string();
case "boolean":
return this.chance.bool();
case "list": {
const res = [];
let size = this.chance.integer({ min: 1, max: 10 });
const childNodeName = this.getTgTypeNameByIndex(typ.items);
while (size--) {
res.push(this.randomizeRecursively(childNodeName));
}
return res;
if (typ.format == "ean") {
return generateEAN(chance);
}
default:
throw new Error(`type not supported "${typ.type}"`);
return chance.string();
case "boolean":
return chance.bool();
case "list": {
const res = [];
let size = chance.integer({ min: 1, max: 10 });
const childNodeName = tgTypes[typ.items];
while (size--) {
res.push(
randomizeRecursively(childNodeName, chance, tgTypes),
);
}
return res;
}

default:
throw new Error(`type not supported "${typ.type}"`);
}
}

function generateEAN(chance: typeof Chance) {
let ean = "0";

for (let i = 1; i <= 11; i++) {
ean += chance.integer({ min: 0, max: 9 }).toString();
}

const checkDigit = calculateCheckDigit(ean);
ean += checkDigit;

return ean;
}

function calculateCheckDigit(ean: string) {
const digits = ean.split("").map(Number);

let sum = 0;
for (let i = 0; i < digits.length; i++) {
sum += (i % 2 === 0) ? digits[i] : digits[i] * 3;
}

const checkDigit = (10 - (sum % 10)) % 10;

return checkDigit.toString();
}
26 changes: 26 additions & 0 deletions typegate/src/typegraph/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { DenoRuntime } from "../runtimes/deno/deno.ts";
import { Runtime } from "../runtimes/Runtime.ts";
import { ensure, ensureNonNullable } from "../utils.ts";
import { typegraph_validate } from "native";
import Chance from "chance";

import {
initAuth,
Expand Down Expand Up @@ -41,6 +42,7 @@ import type {
import { InternalAuth } from "../services/auth/protocols/internal.ts";
import { Protocol } from "../services/auth/protocols/protocol.ts";
import { initRuntime } from "../runtimes/mod.ts";
import randomizeRecursively from "../runtimes/random.ts";

export { Cors, Rate, TypeGraphDS, TypeMaterializer, TypePolicy, TypeRuntime };

Expand Down Expand Up @@ -349,6 +351,30 @@ export class TypeGraph {
);
}

getRandom(
schema: TypeNode,
): number | string | null {
const tgTypes: TypeNode[] = this.tg.types;
let seed = 12; // default seed
if (
this.tg.meta.random_seed !== undefined &&
this.tg.meta.random_seed !== null
) {
seed = this.tg.meta.random_seed;
}
const chance: typeof Chance = new Chance(seed);

try {
const result = randomizeRecursively(schema, chance, tgTypes);

return result;
} catch (_) {
throw new Error(
`invalid type for random injection: ${schema.type}`,
);
}
}

nextBatcher = (
type: TypeNode,
): Batcher => {
Expand Down
4 changes: 4 additions & 0 deletions typegate/src/typegraph/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,9 @@ export type Injection = {
} | {
source: "dynamic";
data: InjectionDataFor_String;
} | {
source: "random";
data: InjectionDataFor_String;
};
export type InjectionDataFor_String = SingleValueFor_String | {
[k: string]: string;
Expand Down Expand Up @@ -503,6 +506,7 @@ export interface TypeMeta {
auths: Auth[];
rate?: Rate | null;
version: string;
random_seed: number | undefined;
}
export interface Queries {
dynamic: boolean;
Expand Down
18 changes: 12 additions & 6 deletions typegate/tests/e2e/typegraph/__snapshots__/typegraph_test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,8 @@ snapshot[`typegraphs creation 1`] = `
"context_identifier": "user",
"local_excess": 5
},
"version": "0.0.3"
"version": "0.0.3",
"random_seed": null
}
}
]'
Expand Down Expand Up @@ -533,7 +534,8 @@ snapshot[`typegraphs creation 2`] = `
},
"auths": [],
"rate": null,
"version": "0.0.3"
"version": "0.0.3",
"random_seed": null
}
}
]\`
Expand Down Expand Up @@ -815,7 +817,8 @@ snapshot[`typegraphs creation 3`] = `
},
"auths": [],
"rate": null,
"version": "0.0.3"
"version": "0.0.3",
"random_seed": null
}
}
]\`
Expand Down Expand Up @@ -1147,7 +1150,8 @@ snapshot[`typegraphs creation 4`] = `
"context_identifier": "user",
"local_excess": 5
},
"version": "0.0.3"
"version": "0.0.3",
"random_seed": null
}
}
]'
Expand Down Expand Up @@ -1354,7 +1358,8 @@ snapshot[`typegraphs creation 5`] = `
},
"auths": [],
"rate": null,
"version": "0.0.3"
"version": "0.0.3",
"random_seed": null
}
}
]\`
Expand Down Expand Up @@ -1636,7 +1641,8 @@ snapshot[`typegraphs creation 6`] = `
},
"auths": [],
"rate": null,
"version": "0.0.3"
"version": "0.0.3",
"random_seed": null
}
}
]\`
Expand Down
48 changes: 48 additions & 0 deletions typegate/tests/random/injection/random_injection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from typegraph.policy import Policy
from typegraph.runtimes.deno import DenoRuntime

from typegraph import Graph, t, typegraph


@typegraph()
def random_injection(g: Graph):
pub = Policy.public()
deno = DenoRuntime()

user = t.struct(
{
"id": t.uuid().from_random(),
"ean": t.ean().from_random(),
"name": t.string(config={"gen": "name"}).from_random(),
"age": t.integer(config={"gen": "age", "type": "adult"}).from_random(),
"married": t.boolean().from_random(),
"birthday": t.datetime().from_random(),
"friends": t.list(t.string(config={"gen": "first"})).from_random(),
"phone": t.string(config={"gen": "phone"}).from_random(),
"gender": t.string(config={"gen": "gender"}).from_random(),
"firstname": t.string(config={"gen": "first"}).from_random(),
"lastname": t.string(config={"gen": "last"}).from_random(),
"occupation": t.string(config={"gen": "profession"}).from_random(),
"street": t.string(config={"gen": "address"}).from_random(),
"city": t.string(config={"gen": "city"}).from_random(),
"postcode": t.string(config={"gen": "postcode"}).from_random(),
"country": t.string(
config={"gen": "country", "full": "true"}
).from_random(),
"uri": t.uri().from_random(),
"hostname": t.string(format="hostname").from_random(),
}
)

random_list = t.struct(
{
"names": t.list(t.string(config={"gen": "name"})).from_random(),
}
)
# Configure random injection seed value or the default will be used
g.configure_random_injection(seed=1)
g.expose(
pub,
randomUser=deno.identity(user),
randomList=deno.identity(random_list),
)
Loading

0 comments on commit bbf0b95

Please sign in to comment.