show collections

This commit is contained in:
2022-08-13 00:41:31 -06:00
parent 62a0a3e437
commit 3d34627024
11 changed files with 402 additions and 35 deletions

View File

@ -10,6 +10,9 @@
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<div>
<collections-list aw-endpoint="https://aw.sa.vin/v1" aw-project="62f5ccd6af025a76edb0"></collections-list>
</div>
<script src="/src/index.tsx" type="module"></script>
</body>

View File

@ -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"
}

94
pnpm-lock.yaml generated
View File

@ -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

1
src/components.tsx Normal file
View File

@ -0,0 +1 @@
import './components/collections';

View File

@ -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<CollectionsList>();
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 (
<div class={'collections-frame'}>
<style>{style}</style>
<style>{customStyles}</style>
{/*<span>{JSON.stringify(collections(), null, " ")}</span>*/}
{collections()?.collections.map((col: Collection) => {
return <div>
<h3>{col.name}</h3>
<span>{col.$id}</span><br></br>
{
col.attributes.map((attr: Attribute) => {
return <div>
<span>{attr.key + ", " + attr.type}</span>
</div>;
})
}
</div>;
})}
</div>
);
});

28
src/components/types.ts Normal file
View File

@ -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
}

View File

@ -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(() => <App />, document.getElementById('root') as HTMLElement);
import './components';
// render(() => <App />, document.getElementById('root') as HTMLElement);

View File

@ -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<CollectionsList> = new ObserveCache<CollectionsList>();
constructor() {
this.awClient = new Client();
}
public getCollections(): Observable<CollectionsList> {
let collectionRequest: () => Promise<CollectionsList> = () => {
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<Models.Execution> {
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<Models.Execution> {
return new Promise<Models.Execution>((resolve, reject) => {
setTimeout(() => {
getExec().then((exec: Models.Execution) => {
resolve(exec);
});
}, 2000);
})
}
function getExec(): Promise<Models.Execution> {
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
}

100
src/services/fetch.ts Normal file
View File

@ -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<T>(url: string): Promise<T> {
return fetchAsync('GET', url);
}
export function post<T>(url: string, body?: any): Promise<T> {
return fetchAsync('POST', url, body);
}
export function postWithFile<T>(url: string, body?: any): Promise<T> {
return fetchWithFile('POST', url, body);
}
export function putWithFile<T>(url: string, body?: any): Promise<T> {
return fetchWithFile('PUT', url, body);
}
export function fetchWithFile<T>(method: string, url: string, body?: any): Promise<T> {
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<T>(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;
}

View File

@ -0,0 +1,43 @@
import { Observable, from } from 'rxjs';
import { get } from './fetch';
import { ObserveService } from './observeService';
interface storedItem<T> {
observeService: ObserveService<T>
bust: boolean
updated: number
}
class ObserveCache<T> {
private cacheStore: { [key: string]: storedItem<T> } = {};
private expiration: number = 5 * 60 * 1000; // 5 minutes
public get(key: string, endpoint: string|(()=>Promise<T>)) {
let now = new Date().getTime();
if (!this.cacheStore[key]) {
function getRequest() {
if (typeof endpoint === 'string') {
return from<Promise<T>>(get<T>(endpoint));
}
return from<Promise<T>>(endpoint());
}
this.cacheStore[key] = {
observeService: new ObserveService<T>(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
}

View File

@ -0,0 +1,19 @@
import {merge, Observable, Subject} from 'rxjs';
import {debounceTime, shareReplay, switchMap} from 'rxjs/operators';
export class ObserveService<T> {
private updateStream: Subject<void>;
public value$: Observable<T>;
// TODO: add timestamp for last updated so the cache can be smart about when to check for new values
constructor(request: ()=>Observable<T>) {
this.updateStream = new Subject<void>();
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();
}
}