Replies: 6 comments 22 replies
|
I think I generally understand your idea. You want to introduce GraphQL in a lightweight manner, without needing more advanced features for now, but you hope to have a complete GraphQL experience someday. The current Could you please further explain what functionality Nested executors aim to achieve? |
|
It would probably be a cheap gql client to be run either on the back or on the front. "cheap" because it would be an overkill to completely mimic a gql client / server and we dont' want to reinvent the square wheel. In itself a resolver is no more than a function i.e. with arguments and a return type + a context (parent, runtime context + "query" info context). GQL is nice because the language understands the relations between types and therefore nest a query within the result of another query. The same way we could chain method in a builder pattern for instance. The idea would be to have those executors to work as builders we can chain and which would eventually return what we asked for. Something like that: const johnUsersWithAddress = await query
.resolve({
users: users({ name: "John" }).resolve({
id: true, // true means the field is kept as is with the same name
familyName: "surname", // renamed from "surname"
address: address().resolve({
city: false, // false means we omit the field
gps: location(),
}),
}),
})
.run(ctx);This would return something like: {
"users": [
{
"id": "u1",
"familyName": "Doe",
"address": {
"gps": {
"lat": 46.5197,
"lon": 6.6323
}
}
},
{
"id": "u2",
"familyName": "Smith",
"address": {
"gps": {
"lat": 47.3769,
"lon": 8.5417
}
}
}
]
}Here is it to mimic the gql syntax, but perhaps we can improve something. Questions:
|
|
In a GraphQL Application, Objects Contain Two Types of Properties: Intrinsic Properties and Derived Properties Consider the following code: // Define intrinsic properties for User
const User = v.object({
id: v.string(),
name: v.string(),
email: v.string(),
addressId: v.string(),
birthDate: v.date(),
})
// Define intrinsic properties for Address
const Address = v.object({
id: v.string(),
street: v.string(),
city: v.string(),
})
const userResolver = resolver.of(User, {
/** address is a derived property of User */
address: field(Address).resolve((user) => db.address.find(user.addressId)),
/** age is a derived property of User */
age: field(v.number()).resolve((user) => {
const now = new Date()
const birthDate = new Date(user.birthDate)
return now.getFullYear() - birthDate.getFullYear()
}),
users: query(v.array(User)).resolve(() => db.users.find()),
})
const addressResolver = resolver.of(Address, {
/** users is a derived property of Address */
users: field(v.array(User))
.input({ name: v.nullish(v.string()) })
.resolve((address, { name }) =>
db.users.find({ addressId: address.id, name: { like: `%${name}%` } })
),
addresses: query(Address)
.input({
treet: v.nullable(v.string()),
city: v.nullable(v.string()),
})
.resolve(({ street, city }) => db.address.find({ street, city })),
})In the code above, the Based on my experience, retrieving all intrinsic properties in backend applications incurs almost no overhead (no additional database queries required), but selecting intrinsic properties on the frontend is cumbersome. Personally, I prefer to forgo the ability to select intrinsic properties in Ideas for Client API Design1. Chained Query Constructionexport const client = toExecutor({
user: userResolver,
address: addressResolver,
})
// Retrieve only intrinsic properties of Address
const addresses1 = await client.query.addresses({ city: "Singapore" })
// Retrieve nested properties
const addresses2 = await client.query.addresses({ city: "Singapore" }).with(
client.address.users({ name: "John" }).with({
// Rename field
year: client.user.age(),
})
)This design aligns with JavaScript conventions for parameter passing, but its downside is verbose syntax. 2. Nested Query Constructionexport const client = toExecutor(userResolver, addressResolver)
// Retrieve only intrinsic properties of Address
const { addresses: addresses1 } = await client.query({ addresses: true })
// Retrieve all Addresses in a specific city
const { addresses: addresses2 } = await client.query({
addresses: {
$city: "Singapore", // Use $ prefix for input parameters (consistent with GraphQL parameter syntax)
},
})
// Retrieve intrinsic properties of Address and its derived property "users"
const { addresses: addresses3 } = await client.query({
addresses: {
$city: "Singapore",
users: 1,
},
})
// Add input parameters for "users"
const { addresses: addresses4 } = await client.query({
addresses: {
$city: "Singapore",
users: {
$name: "John",
age: 1,
},
},
})This design is more concise, but the parameter passing approach may feel unintuitive. 3. Single-Query Nested ConstructionConsidering that parameters are used more frequently than selections, and we usually only call one query per GraphQL request, we could use an API like this: export const client = toExecutor(userResolver, addressResolver)
const addresses1 = await client.query.address({ city: "Singapore" })
// Retrieve intrinsic properties of Address, its derived property "users", and add input parameters
const addresses2 = await client.query.addresses({
city: "Singapore",
$with: {
users: 1,
},
})
// Add input parameters for "users"
const addresses4 = await client.query.addresses({
city: "Singapore",
$with: {
users: {
name: "John",
$with: { age: 1 },
},
},
}) |
|
In
Other features, such as field renaming, have very low usage frequency and therefore don't need to be prioritized in my opinion. |
|
Based on our discussions, the query builder (client) in
To implement these four features, I think the following query builder looks good: const user43 = await client.query.user({
$id: "43", // argument
order: { // include derived fields
$id: 10,
address: 1,
"sameAddress:address": true, // renaming
items: {
product: true,
},
payments: {
"...CardPayment": { // include derived fields for union types
bank: true,
},
},
},
}) |
|
Hello, I spent the day exploring a DSL builder, I'm able to, based on GqlObject and the equivalent of the root query resolvers to recursively build all the intrinsic and deterministic fields required to then build our DSL. In short, given types for instance: type User = GqlObject<"User", CommonProperties & {
firstName:string
surname:string
email:string
address:Address
union:Address | Sale
bestFriend:User
pixels:Array<Array<Pixel>>
}>And derived properties (root one are derived properties) // Create a root query type and add some fields
export type RootQuery = {}
type QueryGetUsers = FieldExtension<RootQuery, "getUsers", Array<User>>
type QueryGetUser = FieldExtension<RootQuery, "getUser", User, {id: "string"}>
// Add a derived field to User
type UserBestFriend = FieldExtension<User, "bestFriend", User>I can have a union of all the possible FieldExtension to care about // The type to add a property field of a given type to another type
export type FieldExtension<
OnType extends GqlObject<string, any>,
FieldName extends string,
FieldType extends MaybeDeepArray<any>,// I was using GqlType instead of any perhaps it could even be any type
Args extends Record<string, unknown> = {},
> = {
/** The GraphQL type on which the field is added */
onType: OnType;
/** The name of the field being added */
fieldName: FieldName;
/** The GraphQL type(s) this field returns */
fieldType: FieldType;
/** The arguments this field accepts */
args: Args;
};For instance I get: QueryGetUsers
|QueryGetUser
| UserBestFriend
| FieldExtension<GqlTypeName<"Address"> & {street:string, city:string, zipcode:string}, "street", string, {}>
| FieldExtension<GqlTypeName<"Address"> & {street:string, city:string, zipcode:string}, "city", string, {}>
| FieldExtension<GqlTypeName<"Address"> & {street:string, city:string, zipcode:string}, "zipcode", string, {}>
| FieldExtensionsUsingGqlObject<GqlTypeName<"Sale"> & {amount:number}>
| FieldExtension<GqlTypeName<"Pixel"> & {r:number, g:number, b:number}, "r", number, {}>
| FieldExtension<GqlTypeName<"Pixel"> & {r:number, g:number, b:number}, "g", number, {}>
| FieldExtension<GqlTypeName<"Pixel"> & {r:number, g:number, b:number}, "b", number, {}>
| FieldExtension<GqlTypeName<"User"> & CommonProperties & {firstName:string, surname:string, email:string, address:Address, union:Address | Sale, bestFriend:User, pixels:Array<Array<Pixel>>}, "createdOn", Date, {}>
| ...Those are the only bricks I identified to then build our DLS from them. I still need to work on, I really hope to have time to work on we have exploration days every 3 months, then I will post this POC when it will be ready. Note that I still don't feel completely confident with a DLS way (typescript neither) my intuition is that it is better to scope the properties and intent with a builder. See you |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
I've realize there is a "client free gql" executor https://gqloom.dev/en/docs/advanced/executor
I haven't seen yet and trace of nesting but I'm wondering it it would worth digging more the concept of those executors so that we could have a gql resolvers without gql. Perhaps it's a stupid idea but I feel like we could spare the cost of converting to a schema to then generate queries back to result and so on. Of course, using a gql client is great for caching, writing proper queries with fragments, having an opinion on subscriptions and everything but in a few case, we might want to simply nest resolvers in a simple way and that's why I'm wondering if the executors can / could achieve such task ?
The idea is that it would not pick specific properties, i.e. all the properties supposed to be returned by default will be returned (because it does not cost anything) however any resolved property would require to be explicitly called (with arguments, context and everything if mandatory). The big pro for this is the progressive adoption for frontend, where one could possibly write the resolvers on the client without any gql and then port the logic in a middle ware the time being. I had to deal with several companies reluctant in adding a new BFF (gql middleware) for some reason while I had to aggregate all their apis in some manner. Reinventing the wheel is quite boring while using an executor would be an excellent first step into a GQL adoption. Just to say: in a former company, I had absolutely no way to host any new server, I therefore ran a gql server on the client, while it worked even with the apollo google extension it did lack of most of the real benefits of gql (n+1, sharing logic, simplified CORS request, server observability and so on).
All reactions