From 3d3462702400b2dcd55ba2382993d3156ad6d32a Mon Sep 17 00:00:00 2001 From: Mason Payne Date: Sat, 13 Aug 2022 00:41:31 -0600 Subject: [PATCH] show collections --- index.html | 3 + package.json | 8 ++- pnpm-lock.yaml | 94 +++++++++++++++++++++---------- src/components.tsx | 1 + src/components/collections.tsx | 51 +++++++++++++++++ src/components/types.ts | 28 +++++++++ src/index.tsx | 8 ++- src/services/aw-service.ts | 82 +++++++++++++++++++++++++++ src/services/fetch.ts | 100 +++++++++++++++++++++++++++++++++ src/services/observeCache.ts | 43 ++++++++++++++ src/services/observeService.ts | 19 +++++++ 11 files changed, 402 insertions(+), 35 deletions(-) create mode 100644 src/components.tsx create mode 100644 src/components/collections.tsx create mode 100644 src/components/types.ts create mode 100644 src/services/aw-service.ts create mode 100644 src/services/fetch.ts create mode 100644 src/services/observeCache.ts create mode 100644 src/services/observeService.ts diff --git a/index.html b/index.html index 127e9fc..15129d4 100644 --- a/index.html +++ b/index.html @@ -10,6 +10,9 @@
+
+ +
diff --git a/package.json b/package.json index 2dbb5ed..ac54f31 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,10 @@ "description": "UI that generates itself based on the shape of a given Appwrite project.", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "start": "vite", + "dev": "vite", + "build": "vite build", + "serve": "vite preview" }, "repository": { "type": "git", @@ -24,7 +27,8 @@ "vite-plugin-solid": "^2.3.0" }, "dependencies": { - "node-appwrite": "^7.0.2", + "appwrite": "^9.0.1", + "rxjs": "^7.5.6", "solid-element": "^1.4.8", "solid-js": "^1.4.7" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 286289f..c79de3f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,7 +1,8 @@ lockfileVersion: 5.4 specifiers: - node-appwrite: ^7.0.2 + appwrite: ^9.0.1 + rxjs: ^7.5.6 solid-element: ^1.4.8 solid-js: ^1.4.7 typescript: ^4.7.4 @@ -9,7 +10,8 @@ specifiers: vite-plugin-solid: ^2.3.0 dependencies: - node-appwrite: 7.0.2 + appwrite: 9.0.1 + rxjs: 7.5.6 solid-element: 1.4.8_solid-js@1.4.8 solid-js: 1.4.8 @@ -387,17 +389,17 @@ packages: color-convert: 1.9.3 dev: true - /asynckit/0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + /appwrite/9.0.1: + resolution: {integrity: sha512-yLxb5H2fqlK0l4q6eEzrb5HGs3xA2894wcLIseOJ2v/iqUmjuIjXfLUpWG+DC94CQmEdZCxwvIFUVO/AG/t+cw==} + dependencies: + cross-fetch: 3.1.5 + isomorphic-form-data: 2.0.0 + transitivePeerDependencies: + - encoding dev: false - /axios/0.27.2: - resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} - dependencies: - follow-redirects: 1.15.1 - form-data: 4.0.0 - transitivePeerDependencies: - - debug + /asynckit/0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: false /babel-plugin-jsx-dom-expressions/0.33.14_@babel+core@7.18.10: @@ -472,6 +474,14 @@ packages: safe-buffer: 5.1.2 dev: true + /cross-fetch/3.1.5: + resolution: {integrity: sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==} + dependencies: + node-fetch: 2.6.7 + transitivePeerDependencies: + - encoding + dev: false + /debug/4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -712,19 +722,9 @@ packages: engines: {node: '>=0.8.0'} dev: true - /follow-redirects/1.15.1: - resolution: {integrity: sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - dev: false - - /form-data/4.0.0: - resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} - engines: {node: '>= 6'} + /form-data/2.5.1: + resolution: {integrity: sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==} + engines: {node: '>= 0.12'} dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 @@ -780,6 +780,12 @@ packages: engines: {node: '>=12.13'} dev: true + /isomorphic-form-data/2.0.0: + resolution: {integrity: sha512-TYgVnXWeESVmQSg4GLVbalmQ+B4NPi/H4eWxqALKj63KsUrcu301YDjBqaOw3h+cbak7Na4Xyps3BiptHtxTfg==} + dependencies: + form-data: 2.5.1 + dev: false + /js-tokens/4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} dev: true @@ -826,13 +832,16 @@ packages: hasBin: true dev: true - /node-appwrite/7.0.2: - resolution: {integrity: sha512-qFahgNKk0qfzoL8/a2v1p/9+EsEuKNutxZqZSlweYi5q4jK3jJDcQZyoPI+B76zmLbYpk7q0zO5LrbfVVkI/kg==} + /node-fetch/2.6.7: + resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true dependencies: - axios: 0.27.2 - form-data: 4.0.0 - transitivePeerDependencies: - - debug + whatwg-url: 5.0.0 dev: false /node-releases/2.0.6: @@ -873,6 +882,12 @@ packages: fsevents: 2.3.2 dev: true + /rxjs/7.5.6: + resolution: {integrity: sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==} + dependencies: + tslib: 2.4.0 + dev: false + /safe-buffer/5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} dev: true @@ -927,10 +942,18 @@ packages: engines: {node: '>=4'} dev: true + /tr46/0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false + /ts-toolbelt/9.6.0: resolution: {integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==} dev: true + /tslib/2.4.0: + resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} + dev: false + /typescript/4.7.4: resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==} engines: {node: '>=4.2.0'} @@ -991,3 +1014,14 @@ packages: optionalDependencies: fsevents: 2.3.2 dev: true + + /webidl-conversions/3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false + + /whatwg-url/5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false diff --git a/src/components.tsx b/src/components.tsx new file mode 100644 index 0000000..1e80d9f --- /dev/null +++ b/src/components.tsx @@ -0,0 +1 @@ +import './components/collections'; diff --git a/src/components/collections.tsx b/src/components/collections.tsx new file mode 100644 index 0000000..e54a1d5 --- /dev/null +++ b/src/components/collections.tsx @@ -0,0 +1,51 @@ +import {createSignal, onMount} from "solid-js"; +import { customElement } from "solid-element"; +import {awService} from "../services/aw-service"; +import {Attribute, Collection, CollectionsList} from "./types"; + +const style = ``; + +const defaultProps = { + awEndpoint: "localhost:80/v1", + awProject: "", + styles: "", +}; + +customElement("collections-list", defaultProps, (props) => { + const [collections, setCollections] = createSignal(); + awService.init(props.awEndpoint, props.awProject); + + let customStyles; + try { + customStyles = props.styles; + } catch(e) { + customStyles = ''; + } + + onMount(async () => { + awService.getCollections().subscribe((collection: CollectionsList) => { + setCollections(collection); + }); + }); + + return ( +
+ + + {/*{JSON.stringify(collections(), null, " ")}*/} + {collections()?.collections.map((col: Collection) => { + return
+

{col.name}

+ {col.$id}

+ { + col.attributes.map((attr: Attribute) => { + return
+ {attr.key + ", " + attr.type} +
; + }) + } +
; + })} +
+ ); +}); diff --git a/src/components/types.ts b/src/components/types.ts new file mode 100644 index 0000000..3ed6ba4 --- /dev/null +++ b/src/components/types.ts @@ -0,0 +1,28 @@ +export interface CollectionsList { + collections: Collection[] + total: number +} + +export interface Collection { + $id: string + $createdAt: number + $updatedAt: number + $read: string[] + $write: string[] + databaseId: string + name: string + enabled: boolean + permission: string + attributes: Attribute[] +} + +export interface Attribute { + key: string + type: string + status: string + required: boolean + array: boolean + size: number + default: any +} + diff --git a/src/index.tsx b/src/index.tsx index 6362c7b..172b51a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,7 +1,9 @@ /* @refresh reload */ -import { render } from 'solid-js/web'; +// import { render } from 'solid-js/web'; import './index.css'; -import App from './App'; +// import App from './App'; -render(() => , document.getElementById('root') as HTMLElement); +import './components'; + +// render(() => , document.getElementById('root') as HTMLElement); diff --git a/src/services/aw-service.ts b/src/services/aw-service.ts new file mode 100644 index 0000000..726851c --- /dev/null +++ b/src/services/aw-service.ts @@ -0,0 +1,82 @@ +import { Observable } from 'rxjs'; +import {ObserveCache} from "./observeCache"; +import {Account, Client, Functions, Models} from 'appwrite'; +import {CollectionsList} from "../components/types"; + +class AWService { + private awEndpoint: string = ""; + private awProject: string = ""; + private readonly awClient: Client; + private collectionsObserveCache: ObserveCache = new ObserveCache(); + + constructor() { + this.awClient = new Client(); + } + + public getCollections(): Observable { + let collectionRequest: () => Promise = () => { + let account = new Account(this.awClient); + let functions = new Functions(this.awClient); + + return account.get().then(() => { + return functions.createExecution("getCollections") + }).then((executionInfo: Models.Execution) => { + return waitForCompletion(); + + async function waitForCompletion(): Promise { + let complete = false; + let exec: Models.Execution = await getExec(); + if (exec.status !== "processing") { + complete = true; + } + while (!complete) { + exec = await delay(); + if (exec.status !== "processing") { + complete = true; + } + } + return exec; + } + + async function delay(): Promise { + return new Promise((resolve, reject) => { + setTimeout(() => { + getExec().then((exec: Models.Execution) => { + resolve(exec); + }); + }, 2000); + }) + } + + function getExec(): Promise { + return functions.getExecution("getCollections", executionInfo.$id); + } + }).then((ex: Models.Execution) => { + return JSON.parse(ex.response) as CollectionsList; + }).catch((err) => { + console.error(err); + return {collections: [], total: 0}; + }); + } + return this.collectionsObserveCache.get('collections', collectionRequest); + } + + public init(endpoint: string, project: string) { + if (this.awEndpoint === "") { + this.awEndpoint = endpoint; + this.awClient.setEndpoint(endpoint); + } + if (this.awProject === "") { + this.awProject = project; + this.awClient.setProject(project); + } + // otherwise we have already been initialized, and we don't need to do anything + } +} + +const awService = new AWService(); + +export { + awService + // AWService +} diff --git a/src/services/fetch.ts b/src/services/fetch.ts new file mode 100644 index 0000000..d9d7316 --- /dev/null +++ b/src/services/fetch.ts @@ -0,0 +1,100 @@ +declare var window: any; +const defaultBasePath = ''; + +export function fetchAsync(method: 'GET' | 'POST' | 'DELETE' | 'PUT', url: string, body?: any) { + const headers = { 'Content-Type': 'application/json; charset=utf-8' }; + // if (access_token) headers['Authorization'] = `Token ${access_token}`; + return window.fetch(`${defaultBasePath}${url}`, { + method, + headers, + body: body && JSON.stringify(body) + }).then((response: any) => { + // if (response.status === 401) { + // window.location.href = '/auth/login'; + // // throw new Error('401'); + // } + // if (response.status === 403 && url === '/api/v1/organization/') { + // // this indicates a user that is not supposed to be in bx-console + // window.location.href = '/auth/logout'; + // } + const result = response.json(); + if (!response.ok) throw result; + return result; + }); +} + +export function get(url: string): Promise { + return fetchAsync('GET', url); +} + +export function post(url: string, body?: any): Promise { + return fetchAsync('POST', url, body); +} + +export function postWithFile(url: string, body?: any): Promise { + return fetchWithFile('POST', url, body); +} + +export function putWithFile(url: string, body?: any): Promise { + return fetchWithFile('PUT', url, body); +} + +export function fetchWithFile(method: string, url: string, body?: any): Promise { + return window.fetch(`${defaultBasePath}${url}`, { + method: method, + body: body + }).then((response: any) => { + if (response.status === 401) { + window.location.href = '/auth/login'; + // throw new Error('401'); + } + const result = response.json(); + if (!response.ok) throw result; + return result; + }) +} + +export function del(url: string) { + return fetchAsync('DELETE', url); +} + +export function put(url: string, body?: any) { + return fetchAsync('PUT', url, body); +} +export function toQueryString(obj: any) { + const parts = []; + for (var i in obj) { + if (obj.hasOwnProperty(i)) { + parts.push(encodeURIComponent(i) + "=" + encodeURIComponent(obj[i])); + } + } + return parts.join("&"); +} + +export function serializeObject(form: any) { + let obj: {[key: string]: any} = {}; + if (typeof form == 'object' && form.nodeName == "FORM") { + for (let i = 0; i < form.elements.length; i++) { + const field = form.elements[i]; + if (field.name + && field.type != 'file' + && field.type != 'reset' + && field.type != 'submit' + && field.type != 'button') { + if (field.type == 'select-multiple') { + obj[field.name] = ''; + let tempvalue = ''; + for (let j = 0; j < form.elements[i].options.length; j++) { + if (field.options[j].selected) + tempvalue += field.options[j].value + ';'; + } + if (tempvalue.charAt(tempvalue.length - 1) === ';') obj[field.name] = tempvalue.substring(0, tempvalue.length - 1); + + } else if ((field.type != 'checkbox' && field.type != 'radio') || field.checked) { + obj[field.name] = field.value; + } + } + } + } + return obj as T; +} diff --git a/src/services/observeCache.ts b/src/services/observeCache.ts new file mode 100644 index 0000000..e69a27a --- /dev/null +++ b/src/services/observeCache.ts @@ -0,0 +1,43 @@ +import { Observable, from } from 'rxjs'; +import { get } from './fetch'; +import { ObserveService } from './observeService'; + +interface storedItem { + observeService: ObserveService + bust: boolean + updated: number +} + +class ObserveCache { + private cacheStore: { [key: string]: storedItem } = {}; + private expiration: number = 5 * 60 * 1000; // 5 minutes + + public get(key: string, endpoint: string|(()=>Promise)) { + let now = new Date().getTime(); + if (!this.cacheStore[key]) { + function getRequest() { + if (typeof endpoint === 'string') { + return from>(get(endpoint)); + } + return from>(endpoint()); + } + this.cacheStore[key] = { + observeService: new ObserveService(getRequest), + bust: false, + updated: now, + }; + } else if (this.cacheStore[key].bust || now - this.cacheStore[key].updated > this.expiration) { + this.cacheStore[key].observeService.next(); + this.cacheStore[key].updated = now; + } + return this.cacheStore[key].observeService.value$; + } + + public bust(key: string) { + this.cacheStore[key].bust = true; + } +} + +export { + ObserveCache +} diff --git a/src/services/observeService.ts b/src/services/observeService.ts new file mode 100644 index 0000000..67d9eec --- /dev/null +++ b/src/services/observeService.ts @@ -0,0 +1,19 @@ +import {merge, Observable, Subject} from 'rxjs'; +import {debounceTime, shareReplay, switchMap} from 'rxjs/operators'; + +export class ObserveService { + private updateStream: Subject; + public value$: Observable; + // TODO: add timestamp for last updated so the cache can be smart about when to check for new values + + constructor(request: ()=>Observable) { + this.updateStream = new Subject(); + const initialCurrentResponse$ = request(); + const reload$ = this.updateStream.pipe(debounceTime(1000), switchMap(() => request())); + this.value$ = merge(initialCurrentResponse$, reload$).pipe(shareReplay(1)); + } + + public next() { + this.updateStream.next(); + } +}