Compare commits

...

9 Commits

Author SHA1 Message Date
Florent BEAUCHAMP
eb110af59b args and schema 2024-03-14 10:56:21 +01:00
Florent BEAUCHAMP
08166f9a18 typo 2024-03-14 10:48:50 +01:00
Florent BEAUCHAMP
544d071288 add global subscription per collection 2024-03-14 10:47:58 +01:00
Florent Beauchamp
bce26a910c feat(graphql): use id to confort appollo client cache 2024-03-12 16:34:44 +00:00
Florent Beauchamp
f5a2de7fec feat(xo-server/graphql): write a schema.graphql + subscrtiption
the schema is in xo-server, updated when the server start
the subscriptions are {type in lowercase} + Removed or Updated.
These subscription take a uuid parameter

subscription {type in lowercase}Added (it's on the collection)
2024-03-08 12:54:53 +00:00
Florent Beauchamp
60d76e6b8e feat: add timestamp 2024-03-08 08:01:00 +00:00
Florent Beauchamp
a28ad1ddeb feat: more types 2024-03-06 15:57:02 +00:00
Florent Beauchamp
45bfd53a40 feat(xo-server/graphql): plug collections 2024-03-06 15:30:25 +00:00
Florent Beauchamp
d6f5c8890e fix(xo-server/graphql): first server 2024-03-06 10:30:18 +00:00
5 changed files with 375 additions and 1 deletions

View File

@@ -78,6 +78,10 @@
"fs-extra": "^11.1.0",
"get-stream": "^7.0.1",
"golike-defer": "^0.5.1",
"graphql": "^15.8.0",
"graphql-http": "^1.22.0",
"graphql-subscriptions": "^2.0.0",
"graphql-ws": "^5.15.0",
"hashy": "^0.11.1",
"helmet": "^3.9.0",
"highland": "^2.11.1",
@@ -115,6 +119,7 @@
"pw": "^0.0.4",
"readable-stream": "^4.1.0",
"redis": "^4.2.0",
"ruru": "^2.0.0-beta.11",
"semver": "^7.3.2",
"serve-static": "^1.13.1",
"set-cookie-parser": "^2.3.5",
@@ -123,6 +128,7 @@
"split2": "^4.1.0",
"stoppable": "^1.0.5",
"subleveldown": "^6.0.1",
"subscriptions-transport-ws": "^0.11.0",
"syslog-client": "^1.1.1",
"tar-stream": "^3.1.6",
"tmp": "^0.2.1",

View File

@@ -0,0 +1,86 @@
schema {
query: XoQueryType
subscription: RootSubscriptionType
}
type XoQueryType {
host(id: ID): host
hosts(limit: Int, offset: Int, filter: String): [host]
vm(id: ID): VM
vms(limit: Int, offset: Int, filter: String): [VM]
vbd(id: ID): VBD
vbds(limit: Int, offset: Int, filter: String): [VBD]
vdi(id: ID): VDI
vdis(limit: Int, offset: Int, filter: String): [VDI]
sr(id: ID): SR
srs(limit: Int, offset: Int, filter: String): [SR]
}
type host {
id: ID
name_label: String
description: String
timestamp: Int
vms(limit: Int, offset: Int, filter: String): [VM]
}
type VM {
id: ID
name_label: String
description: String
power_state: String
timestamp: Int
host(filter: String): host
vbds(limit: Int, offset: Int, filter: String): [VBD]
}
type VBD {
attached: Boolean
bootable: Boolean
is_cd_drive: Boolean
read_only: Boolean
device: String
position: String
timestamp: Int
vm(filter: String): VM
vdi(filter: String): VDI
}
type VDI {
id: ID
name_label: String
description: String
size: Int
usage: Int
timestamp: Int
vbds(limit: Int, offset: Int, filter: String): [VBD]
sr(filter: String): SR
}
type SR {
id: ID
name_label: String
description: String
size: Int
usage: Int
timestamp: Int
vdis(limit: Int, offset: Int, filter: String): [VDI]
}
type RootSubscriptionType {
hostUpdated: host
hostRemoved: host
hostAdded: host
vmUpdated: VM
vmRemoved: VM
vmAdded: VM
vbdUpdated: VBD
vbdRemoved: VBD
vbdAdded: VBD
vdiUpdated: VDI
vdiRemoved: VDI
vdiAdded: VDI
srUpdated: SR
srRemoved: SR
srAdded: SR
}

View File

@@ -273,7 +273,7 @@ async function setUpPassport(express, xo, { authentication: authCfg, http: { coo
}
const SIGNIN_STRATEGY_RE = /^\/signin\/([^/]+)(\/callback)?(:?\?.*)?$/
const UNCHECKED_URL_RE = /(?:^\/rest\/)|favicon|fontawesome|images|styles|\.(?:css|jpg|png)$/
const UNCHECKED_URL_RE = /(?:^\/rest\/)|(?:^\/graphql\/)|favicon|fontawesome|images|styles|\.(?:css|jpg|png)$/
express.use(async (req, res, next) => {
const { url } = req

View File

@@ -0,0 +1,28 @@
import bodyParser from 'body-parser'
import { GraphQLSchema, printSchema } from 'graphql'
import XapiGraphQlBackend from './schema.mjs'
import { createHandler } from 'graphql-http/lib/use/express'
import { useServer } from 'graphql-ws/lib/use/ws'
import { WebSocketServer } from 'ws' // yarn add ws
import fs from 'node:fs/promises'
export default class GraphQlApi {
constructor(app, { express, ...other }) {
if (express === undefined) {
return
}
// console.log(Object.keys(other))
const schema = new GraphQLSchema(new XapiGraphQlBackend(app))
const webSocketServer = new WebSocketServer({
server: express,
path: '/graphql/v0',
})
useServer({ schema }, webSocketServer)
fs.writeFile('./schema.graphql', printSchema(schema)).catch(console.error)
const handler = createHandler({ schema })
express.use('/graphql/v0', bodyParser.json())
express.all('/graphql/v0', handler)
}
}

View File

@@ -0,0 +1,254 @@
// schema.js
import { GraphQLObjectType, GraphQLString, GraphQLID, GraphQLList, GraphQLInt, GraphQLBoolean } from 'graphql'
import { PubSub } from 'graphql-subscriptions'
import * as CM from 'complex-matcher'
import { every } from '@vates/predicates'
import { ifDef } from '@xen-orchestra/defined'
const pubsub = new PubSub()
const handleOptionalUserFilter = filter => filter && CM.parse(filter).createPredicate()
export default class XapiGraphQlSchema {
#app
#query
#subscription
#types
get query() {
return this.#query
}
get subscription() {
return this.#subscription
}
constructor(app) {
this.#app = app
app.objects.on('add', items => {
Object.values(items).forEach(item => {
pubsub.publish(`${item.type.toUpperCase()}_ADDED`, item)
})
})
app.objects.on('update', items => {
Object.values(items).forEach(item => {
pubsub.publish(`${item.type.toUpperCase()}_UPDATED`, item)
})
})
app.objects.on('remove', items => {
Object.keys(items).forEach(id => {
pubsub.publish(`REMOVED`, { id })
})
})
this.#makeTypes()
this.#makeQueries()
this.#makeSubscription()
}
#resolveXapiObject(id) {
return { ...this.#app.getObject(id), timestamp: Math.round(Date.now() / 1000) }
}
#resolveXapiObjects(xapiType, { filter, limit, offset } = {}) {
return Object.values(
this.#app.getObjects({
filter: every(_ => _.type === xapiType, handleOptionalUserFilter(filter)),
limit: ifDef(limit, Number),
offset: ifDef(offset, Number),
})
).map(o => ({ ...o, timestamp: Math.round(Date.now() / 1000) }))
}
#makeTypes() {
this.#types = {}
const self = this
const standardCollectionArgs = {
limit: { type: GraphQLInt },
offset: { type: GraphQLInt },
filter: { type: GraphQLString },
}
this.#types.host = new GraphQLObjectType({
name: 'host',
fields: () => ({
id: { type: GraphQLID },
name_label: { type: GraphQLString },
description: { type: GraphQLString },
timestamp: { type: GraphQLInt },
vms: {
type: GraphQLList(self.#types.VM),
args: standardCollectionArgs,
resolve(parent, args) {
return self.#resolveXapiObjects('VM', args).filter(vm => vm.$container === parent.id)
},
},
}),
})
this.#types.VM = new GraphQLObjectType({
name: 'VM',
fields: () => ({
id: { type: GraphQLID },
name_label: { type: GraphQLString },
description: { type: GraphQLString },
power_state: { type: GraphQLString },
timestamp: { type: GraphQLInt },
host: {
type: self.#types.host,
args: {
filter: { type: GraphQLString },
},
resolve(parent, args) {
return self.#resolveXapiObject(parent.$container)
},
},
vbds: {
type: GraphQLList(self.#types.VBD),
args: standardCollectionArgs,
resolve(parent, args) {
return self.#resolveXapiObjects('VBD', args).filter(vbd => vbd.VM === parent.id)
},
},
}),
})
this.#types.VBD = new GraphQLObjectType({
name: 'VBD',
fields: () => ({
attached: { type: GraphQLBoolean },
bootable: { type: GraphQLBoolean },
is_cd_drive: { type: GraphQLBoolean },
read_only: { type: GraphQLBoolean },
device: { type: GraphQLString },
position: { type: GraphQLString },
timestamp: { type: GraphQLInt },
vm: {
type: self.#types.VM,
args: {
filter: { type: GraphQLString },
},
resolve(parent, args) {
return self.#resolveXapiObject(parent.VM)
},
},
vdi: {
type: self.#types.VDI,
args: {
filter: { type: GraphQLString },
},
resolve(parent, args) {
return self.#resolveXapiObject(parent.VDI)
},
},
}),
})
this.#types.VDI = new GraphQLObjectType({
name: 'VDI',
fields: () => ({
id: { type: GraphQLID },
name_label: { type: GraphQLString },
description: { type: GraphQLString },
size: { type: GraphQLInt },
usage: { type: GraphQLInt },
timestamp: { type: GraphQLInt },
vbds: {
type: GraphQLList(self.#types.VBD),
args: standardCollectionArgs,
resolve(parent, args) {
return self.#resolveXapiObjects('VBD', args).filter(vbd => vbd.VDI === parent.id)
},
},
sr: {
type: self.#types.SR,
args: {
filter: { type: GraphQLString },
},
resolve(parent, args) {
return self.#resolveXapiObject(parent?.$SR)
},
},
}),
})
this.#types.SR = new GraphQLObjectType({
name: 'SR',
fields: () => ({
id: { type: GraphQLID },
name_label: { type: GraphQLString },
description: { type: GraphQLString },
size: { type: GraphQLInt },
usage: { type: GraphQLInt },
timestamp: { type: GraphQLInt },
vdis: {
type: GraphQLList(self.#types.VDI),
args: standardCollectionArgs,
resolve(parent, args) {
return self.#resolveXapiObjects('VDI', args).filter(vdi => vdi.SR === parent.id)
},
},
}),
})
}
#makeQueries() {
const self = this
const fields = {}
Object.entries(this.#types).forEach(([typeName, type]) => {
fields[typeName.toLocaleLowerCase()] = {
type,
args: {
id: { type: GraphQLID },
},
resolve(parent, args) {
return self.#resolveXapiObject(args.id)
},
}
fields[typeName.toLocaleLowerCase() + 's'] = {
type: new GraphQLList(type),
args: {
limit: { type: GraphQLInt },
offset: { type: GraphQLInt },
filter: { type: GraphQLString },
},
resolve(parent, args) {
return self.#resolveXapiObjects(typeName, args)
},
}
})
this.#query = new GraphQLObjectType({
name: 'XoQueryType',
fields,
})
}
#makeSubscription() {
const fields = {}
Object.entries(this.#types).forEach(([typeName, type]) => {
fields[`${typeName.toLocaleLowerCase()}Updated`] = {
type,
subscribe: (parent, args) => {
return pubsub.asyncIterator(`${typeName.toUpperCase()}_UPDATED`)
},
resolve: payload => payload,
}
fields[`${typeName.toLocaleLowerCase()}Removed`] = {
type,
// args: { id: { type: new GraphQLNonNull(GraphQLID) } },
subscribe: (parent, args) => {
return pubsub.asyncIterator(`${typeName.toUpperCase()}_REMOVED`)
},
resolve: payload => payload,
}
fields[typeName.toLocaleLowerCase() + 'Added'] = {
type: type,
subscribe(parent, args) {
return pubsub.asyncIterator(`${typeName.toUpperCase()}_ADDED`)
},
resolve: payload => payload,
}
})
this.#subscription = new GraphQLObjectType({
name: 'RootSubscriptionType',
fields,
})
}
}