Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Example of proper way to consume types in the callbacks? #45

Open
DillonSadofsky opened this issue Sep 3, 2024 · 1 comment
Open

Example of proper way to consume types in the callbacks? #45

DillonSadofsky opened this issue Sep 3, 2024 · 1 comment

Comments

@DillonSadofsky
Copy link

DillonSadofsky commented Sep 3, 2024

So I am trying to add SCIM support using SCIMMY and SCIMMYRouters to my express API. So far so good. Everything is hooked up, and my placeholder ingress/egress functions are being hit by my postman tests.

However, I saw that you had type definitions but I can't seem to find a reasonable way to use them in my modules. I was having trouble importing types from the module, possibly because the types aren't reexported at the top level of the module? IDK, I was able to get around that by typing inputs using typeof, but the objects passed to my callbacks don't seem to match these classes members, so its not quite right and would welcome input.

For example, for ingress, it is documented with a js example at https://scimmyjs.github.io/SCIMMY.Types.Resource.html#~IngressHandler (thanks for that) with the arguments types listed as SCIMMY.Types.Resource and SCIMMY.Types.Schema

I tried converting that literally:

	// Register the User resource
	SCIMMY.Resources.declare(SCIMMY.Resources.User, {})
	// Add support for the optional EnterpriseUser extension
	SCIMMY.Resources.User.extend(SCIMMY.Schemas.EnterpriseUser, false)
	// Add handlers for retrieval and submission
	SCIMMY.Resources.User.ingress(async (resource: typeof SCIMMY.Types.Resource, data: typeof SCIMMY.Resources.User.schema, _ctx: unknown): Promise<typeof SCIMMY.Schemas.User> => {
		console.log(`Request to create/edit user ${JSON.stringify(resource)}, ${JSON.stringify(data)}`)

                // For now, return a raw Promise, eventually we'll await database operations
		return await new Promise((resolve) => resolve({ ...data, id: '456' }))
	})
	SCIMMY.Resources.User.egress((resource: Record<string, unknown>) => {
		console.log(`Request to read user ${JSON.stringify(resource)}`)
	})
	SCIMMY.Resources.User.degress((resource: Record<string, unknown>) => {
		console.log(`Request to delete user ${JSON.stringify(resource)}`)
	})

But those types' members don't seem to match the members of the object resource/data. I also tried typing using SCIMMY.Resources.User and even SCIMMY.Resources.User.definition based on following steps through the constructors.

Anyway, I get that part of this is probably because the actual attributes on an object are defined by the schema, and can be optionally present, but it seemed like in schemas.js User.#definition did some magic, so I wasn't sure if there was a way for me to type the objects such that typescript would know what the possible (they could be missing on a given call I know) members supplied by the User or Group object would be? I assume that is kinda the point of these resources? To type them?

Maybe I'm missing something obvious, but I've been chasing my tail trying to use SCIMMY.Resources.User, SCIMMY.Schemas.User, SCIMMY.Types.User, etc but I can't seem to figure it out. Maybe its not possible and everyone just types the incoming data as unknown or Record<string, unknown> and test everything manually. Let me know if that is true, but it seems like this library validates the input before calling my handler, so it would suck to have to treat the object as an anonymous, untyped object when it has been verified to meet a, well, type.

Anyway, I'm liking the library so far, just hoping to add typing into my use case to add auto-complete and transpile-time error checking.

And if I am just missing something, I'd suggest changing the type of the handler argument to SCIMMY.Resources.User.ingress/etc. Currently it explicitly seems to take any, and it'd at least be cool if it typed its input as a callback that took the three arguments and returns a promise of an object of the requested shape. It'd take some of the guesswork out of making the handler implementation. I think it'd even help non-TS developers since VSCode will show hints based on types even in pure JS these days.

@DillonSadofsky
Copy link
Author

DillonSadofsky commented Sep 4, 2024

In the end, I found this type magic to get me relatively close. Its been harder than consuming types from some libraries, and for a bit I thought it was impossible without modifying the .d.ts to explicitly export some more types, but this seems to be getting close (implementation and product specific details removed for space).

import type { Express } from 'express'
import SCIMMY from 'scimmy'
import SCIMMYRouters from 'scimmy-routers'
import { parse as SCIMParse, filter as SCIMFilter } from 'scim2-parse-filter'

type SCIMMYUserSchema = InstanceType<typeof SCIMMY.Schemas.User>
// An arbitrary object.  Technically its constrainted at runtime, but there doesn't seem to be a type for that?
type SCIMMYData = Record<string, any>
// The way the constructor of the User object works, it copies all the input data on to extend it
type SCIMMYUserInput = SCIMMYUserSchema | SCIMMYData

type SCIMMYResource = InstanceType<typeof SCIMMY.Types.Resource>
type SCIMMYConstraints = SCIMMYResource['constraints']
type SCIMMYFilter = SCIMMYResource['filter']

// As part of the RFC, the multi-objects have some basic properties
type SCIMMulti = {
	type: /*'home' | 'work' | 'other' |*/ string
	primary: boolean
	display: string
	value: string
}

type SCIMEmail = SCIMMulti
type Context = {
	// My product context
}
// Manages state, registers routes, contains the controller that performs product specific operations
class SCIMHandler<UserEntity, Controller extends SCIMController<UserEntity>> {
	controller: Controller

	applyFilter(data: Array<SCIMMYData>, filter: SCIMMYFilter) {
		return data.filter(SCIMFilter(SCIMParse(filter.expression)))
	}

	ingress = async (resource: SCIMMYResource, data: SCIMMYUserInput, ctx: Context): Promise<SCIMMYData> => {
		// Implementation removed for space
	}

	egress = async (resource: SCIMMYResource, ctx: Context) => {
		// Implementation removed for space
	}

	degress = (resource: SCIMMYResource, ctx: Context) => {
		// Implementation removed for space
	}

	// Main entrypoint for adding SCIM functionality to an Express server
	registerSCIMRoutes(app: Express) {
		const scimConfig = SCIMMY.Config.get() as Record<string, unknown>
		console.log(scimConfig)

		// Register the User resource
		SCIMMY.Resources.declare(SCIMMY.Resources.User, {})
		// Add support for the optional EnterpriseUser extension
		SCIMMY.Resources.User.extend(SCIMMY.Schemas.EnterpriseUser, false)
		// Add handlers for retrieval and submission
		SCIMMY.Resources.User.ingress(this.ingress)
		SCIMMY.Resources.User.egress(this.egress)
		SCIMMY.Resources.User.degress(this.degress)
	}
}

The types at least seem to line up for the resource.filter, resource.id, resource.constraints. Not so much for the actual User fields, so I've had to just use the names of the attributes at runtime or the RFC to build the marshall/unmarshall transforms for going back and forth between a raw SCIM object and one of my domain UserAccount entity objects. So I still welcome input if there is something I'm missing. And if any other TS users come through wanting to know how to type ingress/egress/etc maybe the above would be useful to them. I think the biggest issue was just that the types aren't reexported like the static objects are, so in some libraries one would do something like import type { UserResource } from 'scimmy' you can't do that here, and there are no 'hints' from the typing of the argument to when you register your ingress/egress since it takes argument any instead of something like (Resource, User, Ctx) => ?, which would flow to the function or give a consumer some idea of what to import, type-wise, to do it themselves.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

1 participant