Skip to content

Commit

Permalink
feat: schema transforms
Browse files Browse the repository at this point in the history
  • Loading branch information
Akryum committed Feb 5, 2024
1 parent e8b3d02 commit 6f2fcbd
Show file tree
Hide file tree
Showing 14 changed files with 270 additions and 116 deletions.
2 changes: 1 addition & 1 deletion packages/app/server/api/fieldActions/counts.get.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export default defineEventHandler(async () => {
const mq = getMq()
const ctx = await mq.getResolvedContext()
const actions = ctx.fieldActions.allActions.map(fa => ({
const actions = ctx.fieldActions.items.map(fa => ({
resourceName: fa.resourceName,
fieldName: fa.fieldName,
file: fa.file,
Expand Down
2 changes: 1 addition & 1 deletion packages/app/server/api/fieldActions/index.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export default defineEventHandler(async (event) => {

const mq = getMq()
const ctx = await mq.getResolvedContext()
let result = ctx.fieldActions.allActions.map(fa => ({
let result = ctx.fieldActions.items.map(fa => ({
resourceName: fa.resourceName,
fieldName: fa.fieldName,
file: fa.file,
Expand Down
2 changes: 1 addition & 1 deletion packages/app/server/api/fieldActions/preview.post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default defineEventHandler(async (event) => {
const mq = getMq()
const ctx = await mq.getResolvedContext()

const fieldAction = ctx.fieldActions.allActions.find(fa => fa.resourceName === resourceName && fa.fieldName === fieldName)
const fieldAction = ctx.fieldActions.items.find(fa => fa.resourceName === resourceName && fa.fieldName === fieldName)
if (!fieldAction) {
throw new Error(`Field action not found for resource ${resourceName} and field ${fieldName}`)
}
Expand Down
54 changes: 43 additions & 11 deletions packages/core/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { type SettingsManager, createSettingsManager } from './settings/settings
import { type PubSubs, createPubSubs } from './pubsub/createPubSub.js'
import type { MoquerieInstance } from './instance.js'
import type { HistoryRecord } from './types/history.js'
import { SchemaTransformStore } from './resource/schemaTransformStore.js'

export interface Context {
contextWatcher: FSWatcher
Expand Down Expand Up @@ -103,6 +104,7 @@ export interface ResolvedContext {
server: Server
mockFiles: MockFileWatcher
fieldActions: FieldActionStore
schemaTransforms: SchemaTransformStore
// @TODO type
db: QueryManagerProxy
// @TODO type
Expand All @@ -114,17 +116,7 @@ export interface ResolvedContext {
async function createResolvedContext(mq: MoquerieInstance): Promise<ResolvedContext> {
const ctx = await mq.getContext()

const { schema, graphqlSchema } = await getResourceSchema(mq)

let server = mq.data.resolvedContext?.server
if (server) {
if (mq.data.resolvedContext?.context.port !== ctx.port) {
await server.restart()
}
}
else {
server = await createServer(mq)
}
// Mock files

let mockFileWatcher = mq.data.resolvedContext?.mockFiles
const isMockFileWatcherNew = !mockFileWatcher
Expand All @@ -134,6 +126,8 @@ async function createResolvedContext(mq: MoquerieInstance): Promise<ResolvedCont
mq.onDestroy(() => mockFileWatcher?.destroy())
}

// Field actions

let fieldActions = mq.data.resolvedContext?.fieldActions

if (!fieldActions) {
Expand All @@ -143,17 +137,55 @@ async function createResolvedContext(mq: MoquerieInstance): Promise<ResolvedCont
mq.onDestroy(() => fieldActions?.destroy())
}

// Schema transforms

let schemaTransforms = mq.data.resolvedContext?.schemaTransforms

if (!schemaTransforms) {
schemaTransforms = new SchemaTransformStore()
mockFileWatcher.onUpdate(schemaTransforms.handleMockFile.bind(schemaTransforms))
mockFileWatcher.onRemove(schemaTransforms.handleMockFileRemoved.bind(schemaTransforms))
mq.onDestroy(() => schemaTransforms?.destroy())

// Update schema
schemaTransforms.onChange(async () => {
if (mq.data.resolvedContext && schemaTransforms) {
const { schema } = await getResourceSchema(mq, schemaTransforms)
mq.data.resolvedContext.schema = schema
}
})
}

if (isMockFileWatcherNew) {
await mockFileWatcher.waitForReady()
}

// Schema

const { schema, graphqlSchema } = await getResourceSchema(mq, schemaTransforms)

// API Server

let server = mq.data.resolvedContext?.server
if (server) {
if (mq.data.resolvedContext?.context.port !== ctx.port) {
await server.restart()
}
}
else {
server = await createServer(mq)
}

// Create context object

return {
context: ctx,
schema,
graphqlSchema,
server,
mockFiles: mockFileWatcher,
fieldActions,
schemaTransforms,
db: createQueryManagerProxy(mq),
pubSubs: mq.data.resolvedContext?.pubSubs ?? await createPubSubs(),
jiti: mq.data.resolvedContext?.jiti ?? createJITI(mq.data.cwd, {
Expand Down
55 changes: 20 additions & 35 deletions packages/core/src/fieldActions/fieldActionStore.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,28 @@
import { MockFileHandler } from '../mock/mockFileHandler.js'
import type { FieldAction } from '../types/fieldAction.js'

export class FieldActionStore {
allActions: FieldAction[] = []

handleMockFile(file: string, data: any) {
// Cleanup
this.allActions = this.allActions.filter(action => action.file !== file)

if (data.__fieldActions) {
const actionsMap = data.__fieldActions
for (const resourceName in actionsMap) {
const actions = actionsMap[resourceName]
for (const fieldName in actions) {
const existingIndex = this.allActions.findIndex(action => action.resourceName === resourceName && action.fieldName === fieldName)
if (existingIndex !== -1) {
this.allActions.splice(existingIndex, 1)
}
export class FieldActionStore extends MockFileHandler<FieldAction> {
constructor() {
super('__fieldActions')
}

const action = actions[fieldName]
this.allActions.push({
resourceName,
fieldName,
action,
file,
})
add(file: string, data: any): void {
for (const resourceName in data) {
const actions = data[resourceName]
for (const fieldName in actions) {
const existingIndex = this.items.findIndex(action => action.resourceName === resourceName && action.fieldName === fieldName)
if (existingIndex !== -1) {
this.items.splice(existingIndex, 1)
}

const action = actions[fieldName]
this.items.push({
resourceName,
fieldName,
action,
file,
})
}
}
}

handleMockFileRemoved(file: string) {
this.removeFieldActions(file)
}

removeFieldActions(file: string) {
this.allActions = this.allActions.filter(action => action.file !== file)
}

destroy() {
this.allActions.length = 0
}
}
2 changes: 1 addition & 1 deletion packages/core/src/graphql/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export async function createGraphQLResolvers(mq: MoquerieInstance): Promise<IRes
}

// Field actions
for (const fieldAction of ctx.fieldActions.allActions) {
for (const fieldAction of ctx.fieldActions.items) {
const { resourceName, fieldName, action } = fieldAction
if (!resolvers[resourceName]) {
resolvers[resourceName] = {}
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/mock/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './mockFileHandler.js'
export * from './mockFileWatcher.js'
52 changes: 52 additions & 0 deletions packages/core/src/mock/mockFileHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export interface MockFileItem<ActionContext> {
file: string
action: (ctx: ActionContext) => any
}

export class MockFileHandler<TItem extends MockFileItem<any>> {
key: string
items: TItem[] = []
changeHandlers: Array<() => unknown> = []

constructor(key: string) {
this.key = key
}

handleMockFile(file: string, data: any) {
// Cleanup
this.removeMany(file)

if (data[this.key]) {
this.add(file, data[this.key])
this.notifyChange()
}
}

// eslint-disable-next-line unused-imports/no-unused-vars
add(file: string, data: any) {
// @TODO: Implement
}

handleMockFileRemoved(file: string) {
this.removeMany(file)
this.notifyChange()
}

removeMany(file: string) {
this.items = this.items.filter(item => item.file !== file)
}

onChange(handler: () => unknown) {
this.changeHandlers.push(handler)
}

destroy() {
this.items.length = 0
}

private notifyChange() {
for (const handler of this.changeHandlers) {
handler()
}
}
}
5 changes: 0 additions & 5 deletions packages/core/src/mock/mockFileWatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@ import createJITI, { type JITI } from 'jiti'
import path from 'pathe'
import type { MoquerieInstance } from '../instance.js'

export interface MockFileHandler {
handleMockFile(file: string, data: any): unknown
handleMockFileRemoved(file: string): unknown
}

export class MockFileWatcher {
watcher: FSWatcher

Expand Down
38 changes: 36 additions & 2 deletions packages/core/src/resource.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { ResourceSchema } from './types/resource.js'
import type { ResourceSchema, ResourceSchemaField } from './types/resource.js'
import type { ResolvedGraphQLSchema } from './graphql/schema.js'
import type { MoquerieInstance } from './instance.js'
import type { SchemaTransformStore } from './resource/schemaTransformStore.js'

export async function getResourceSchema(mq: MoquerieInstance) {
export async function getResourceSchema(mq: MoquerieInstance, schemaTransformStore: SchemaTransformStore) {
const ctx = await mq.getContext()

const schema: ResourceSchema = {
Expand All @@ -19,6 +20,39 @@ export async function getResourceSchema(mq: MoquerieInstance) {
Object.assign(schema.types, resourceSchemaFromGraphQL.types)
}

for (const transform of schemaTransformStore.items) {
await transform.action({
schema,
createField: (name, type, options) => {
return {
name,
type,
array: false,
...options,
tags: ['internal', ...options?.tags ?? []],
} as ResourceSchemaField
},
createEnumField: (name, values) => {
return {
name,
type: 'enum',
values,
array: false,
tags: ['internal'],
}
},
createResourceField: (name, resourceName, array = false) => {
return {
name,
type: 'resource',
resourceName,
array,
tags: ['internal'],
}
},
})
}

return {
schema,
graphqlSchema,
Expand Down
25 changes: 25 additions & 0 deletions packages/core/src/resource/schemaTransformStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { MockFileHandler } from '../mock/mockFileHandler.js'
import type { SchemaTransform } from '../types/resource.js'

export class SchemaTransformStore extends MockFileHandler<SchemaTransform> {
constructor() {
super('__schemaTransforms')
}

add(file: string, data: any): void {
if (Array.isArray(data)) {
for (const transform of data) {
this.items.push({
file,
action: transform,
})
}
}
else {
this.items.push({
file,
action: data,
})
}
}
}
18 changes: 16 additions & 2 deletions packages/core/src/types/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ export type ResourceSchemaCommon = {
name: string
tags: string[]
description?: string
nonNull: boolean
isDeprecated: boolean
nonNull?: boolean
isDeprecated?: boolean
deprecationReason?: string
} & (
{
Expand Down Expand Up @@ -106,3 +106,17 @@ export interface ResourceInstanceReference {
}

export type FilterActive = 'active' | 'inactive' | 'all'

export interface SchemaTransformContext {
schema: ResourceSchema
createField: (name: string, type: ResourceSchemaField['type'], options?: Partial<Omit<ResourceSchemaField, 'name' | 'type'>>) => ResourceSchemaField
createResourceField: (name: string, resourceName: string, array?: boolean) => ResourceSchemaField & { type: 'resource' }
createEnumField: (name: string, values: ResourceSchemaFieldEnumValue[]) => ResourceSchemaField & { type: 'enum' }
}

export type SchemaTransformAction = (ctx: SchemaTransformContext) => any

export interface SchemaTransform {
action: SchemaTransformAction
file: string
}
8 changes: 7 additions & 1 deletion packages/moquerie/src/mocks.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import type { FieldActionBaseDefinitions, ResourceFactoryFn, ResourceFactoryInfo } from '@moquerie/core'
import type { FieldActionBaseDefinitions, ResourceFactoryFn, ResourceFactoryInfo, SchemaTransformAction } from '@moquerie/core'

export function defineFieldActions<TActions extends FieldActionBaseDefinitions>(actions: TActions) {
return {
__fieldActions: actions,
}
}

export function defineSchemaTransforms(handlers: SchemaTransformAction | Array<SchemaTransformAction>) {
return {
__schemaTransforms: handlers,
}
}

export function defineFactory<TInfo extends ResourceFactoryInfo, TFn extends ResourceFactoryFn>(info: TInfo, fn: TFn) {
return {
info,
Expand Down
Loading

0 comments on commit 6f2fcbd

Please sign in to comment.