├── esm ├── package.json └── lib │ └── index.js ├── lib ├── util │ ├── is-node.ts │ ├── is-node-browser.ts │ ├── Class.ts │ ├── atob-browser.ts │ ├── Base64.ts │ ├── Mfa.ts │ ├── atob.ts │ ├── trailingSlashIt.ts │ ├── Json.ts │ ├── uuid.ts │ ├── hmac.ts │ ├── enumerable.ts │ ├── index.ts │ ├── hmac-browser.ts │ ├── atob-rn.ts │ ├── openWindow.ts │ ├── deprecated.ts │ └── Lockable.ts ├── caching │ ├── index.ts │ └── BloomFilter.ts ├── partialupdate │ ├── index.ts │ ├── UpdateOperation.ts │ └── EntityPartialUpdateBuilder.ts ├── error │ ├── RollbackError.ts │ ├── index.ts │ ├── MFAError.ts │ ├── IllegalEntityError.ts │ ├── EntityExistsError.ts │ ├── CommunicationError.ts │ └── PersistentError.ts ├── query │ ├── index.ts │ ├── Operator.ts │ ├── Filter.ts │ └── Builder.ts ├── model │ └── index.ts ├── connector │ ├── index.ts │ ├── FetchConnector.ts │ └── IFrameConnector.ts ├── intersection │ ├── index.ts │ ├── ValidationResult.ts │ ├── GlobalStorage.ts │ ├── WebStorage.ts │ ├── Modules.ts │ ├── PushMessage.ts │ ├── TokenStorage.ts │ ├── Validator.ts │ └── Permission.ts ├── binding │ ├── Accessor.ts │ ├── index.ts │ ├── Role.ts │ ├── User.ts │ ├── ManagedFactory.ts │ ├── Managed.ts │ ├── Factory.ts │ ├── EntityFactory.ts │ ├── DeviceFactory.ts │ └── Enhancer.ts ├── metamodel │ ├── CollectionAttribute.ts │ ├── index.ts │ ├── EmbeddableType.ts │ ├── PluralAttribute.ts │ ├── SingularAttribute.ts │ ├── DbIndex.ts │ ├── ListAttribute.ts │ ├── SetAttribute.ts │ ├── Type.ts │ ├── MapAttribute.ts │ └── ModelBuilder.ts ├── index.ts ├── deperecated-exports.ts ├── baqend.ts └── GeoPoint.ts ├── .commitlintrc.json ├── spec ├── assets │ ├── flames.png │ ├── rocket.jpg │ └── test.json ├── env.js ├── node.js ├── globalDB.spec.js ├── geopoint.spec.js ├── connect.spec.js ├── helper.js ├── emf.spec.js └── pushnotification.spec.js ├── tsconfig.cli.json ├── tsconfig.lib.json ├── .gitignore ├── .npmignore ├── spec-ts ├── tsconfig.json ├── types.d.ts └── typings.spec.ts ├── mocharc.cjs ├── .gitattributes ├── tsconfig.json ├── web-test-runner.config.mjs ├── LICENSE.md ├── web-test-runner.pw.config.mjs ├── orestes-js.iml ├── .releaserc.json ├── cli ├── download.ts ├── helper.ts ├── schema.ts ├── copy.ts └── index.ts ├── web-test-runner.bs.config.mjs ├── webpack.config.js ├── connect.js ├── scripts └── publish.sh ├── CONTRIBUTING.md ├── .eslintrc.js └── package.json /esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } -------------------------------------------------------------------------------- /lib/util/is-node.ts: -------------------------------------------------------------------------------- 1 | export const isNode = true; 2 | -------------------------------------------------------------------------------- /lib/util/is-node-browser.ts: -------------------------------------------------------------------------------- 1 | export const isNode = false; 2 | -------------------------------------------------------------------------------- /lib/caching/index.ts: -------------------------------------------------------------------------------- 1 | export { BloomFilter } from './BloomFilter'; 2 | -------------------------------------------------------------------------------- /lib/util/Class.ts: -------------------------------------------------------------------------------- 1 | export type Class = { new(...args: any[]): T }; 2 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } -------------------------------------------------------------------------------- /lib/util/atob-browser.ts: -------------------------------------------------------------------------------- 1 | export const atob = window.atob.bind(window); 2 | -------------------------------------------------------------------------------- /spec/assets/flames.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Baqend/js-sdk/HEAD/spec/assets/flames.png -------------------------------------------------------------------------------- /spec/assets/rocket.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Baqend/js-sdk/HEAD/spec/assets/rocket.jpg -------------------------------------------------------------------------------- /lib/util/Base64.ts: -------------------------------------------------------------------------------- 1 | export type Base64 = `data:image/${ImageType};base64${string}`; -------------------------------------------------------------------------------- /lib/util/Mfa.ts: -------------------------------------------------------------------------------- 1 | import { Base64 } from './Base64'; 2 | 3 | export type MFAResponse = { 4 | qrCode: Base64<'png'> 5 | keyUri: string 6 | }; 7 | -------------------------------------------------------------------------------- /tsconfig.cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "cli" 5 | ], 6 | "exclude": [ 7 | "lib" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "lib", 5 | ], 6 | "excludeing": [ 7 | "cli" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /lib/partialupdate/index.ts: -------------------------------------------------------------------------------- 1 | export { EntityPartialUpdateBuilder } from './EntityPartialUpdateBuilder'; 2 | export { PartialUpdateBuilder } from './PartialUpdateBuilder'; 3 | -------------------------------------------------------------------------------- /spec/env.js: -------------------------------------------------------------------------------- 1 | var env = { 2 | TEST_SERVER: 'https://local.baqend.com:8443/v1', 3 | }; 4 | 5 | if (typeof module !== 'undefined') { 6 | module.exports = env; 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.grunt 3 | /node_modules 4 | /build 5 | /dist 6 | /commonjs 7 | /doc 8 | /spec-ts/*.js 9 | /lib/*.js 10 | /cli/*.js 11 | /certs 12 | .DS_Store 13 | *.iml 14 | *result.xml 15 | 16 | .watchmanconfig 17 | -------------------------------------------------------------------------------- /lib/partialupdate/UpdateOperation.ts: -------------------------------------------------------------------------------- 1 | export class UpdateOperation { 2 | /** 3 | * @param name 4 | * @param path 5 | * @param [value] 6 | */ 7 | constructor(public name: string, public path: string, public value?: any) {} 8 | } 9 | -------------------------------------------------------------------------------- /lib/error/RollbackError.ts: -------------------------------------------------------------------------------- 1 | import { PersistentError } from './PersistentError'; 2 | 3 | export class RollbackError extends PersistentError { 4 | constructor(cause: Error) { 5 | super('The transaction has been roll backed', cause); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.grunt 3 | /node_modules 4 | /scripts 5 | /build 6 | /tpl 7 | *.iml 8 | .DS_Store 9 | .eslintrc.* 10 | .gitattributes 11 | .releaserc.* 12 | .commitlintrc.* 13 | webpack.config.* 14 | tsconfig.* 15 | mocharc.* 16 | .gitlab-ci.yml 17 | web-test-runner.* 18 | -------------------------------------------------------------------------------- /lib/util/atob.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts Base64-encoded data to string. 3 | * 4 | * @param input Base64-encoded data. 5 | * @return Binary-encoded data string. 6 | */ 7 | export function atob(input: string): string { 8 | return Buffer.from(input, 'base64').toString('binary'); 9 | } 10 | -------------------------------------------------------------------------------- /spec-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "strict": true, 6 | "baseUrl": ".", 7 | "paths": { 8 | "baqend": [".."] 9 | } 10 | }, 11 | "files": [ 12 | "typings.spec.ts", 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /lib/error/index.ts: -------------------------------------------------------------------------------- 1 | export { PersistentError } from './PersistentError'; 2 | export { CommunicationError } from './CommunicationError'; 3 | export { IllegalEntityError } from './IllegalEntityError'; 4 | export { EntityExistsError } from './EntityExistsError'; 5 | export { RollbackError } from './RollbackError'; 6 | -------------------------------------------------------------------------------- /lib/error/MFAError.ts: -------------------------------------------------------------------------------- 1 | import { PersistentError } from './PersistentError'; 2 | 3 | export class MFAError extends PersistentError { 4 | /** 5 | * The Verification Token for the MFA Message 6 | */ 7 | public readonly token: string; 8 | 9 | constructor(token: string) { 10 | super('MFA Required'); 11 | this.token = token; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/util/trailingSlashIt.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Adds a trailing slash to a string if it is missing 3 | * @param str 4 | * @return 5 | * @name trailingSlashIt 6 | * @memberOf prototype 7 | * @function 8 | */ 9 | export function trailingSlashIt(str: string): string { 10 | if (str.charAt(str.length - 1) !== '/') { 11 | return `${str}/`; 12 | } 13 | 14 | return str; 15 | } 16 | -------------------------------------------------------------------------------- /mocharc.cjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | reporter: 'mocha-junit-reporter', 3 | quiet: false, 4 | package: './package.json', 5 | reporterOption: [ 6 | `mochaFile=./node-result.xml`, 7 | 'jenkinsMode=false', 8 | `rootSuiteTitle="Node ${process.version} Test Suite"`, 9 | ], 10 | timeout: 4000, 11 | spec: ['spec/**/*.js'], 12 | } 13 | 14 | module.exports = config; 15 | -------------------------------------------------------------------------------- /spec/node.js: -------------------------------------------------------------------------------- 1 | // initialize Test Framework 2 | var chai = require('chai'); 3 | 4 | chai.config.includeStack = true; 5 | 6 | global.expect = chai.expect; 7 | 8 | // load SDK 9 | global.Baqend = require('..'); 10 | 11 | // initialize tests 12 | global.env = require('./env'); 13 | global.helper = require('./helper'); 14 | 15 | // init helper libs 16 | global.OTPAuth = require('otpauth'); 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | *.cs text diff=csharp 5 | *.java text diff=java 6 | *.html text diff=html 7 | *.css text 8 | *.js text 9 | *.sql text 10 | 11 | *.csproj text merge=union 12 | *.sln text merge=union eol=crlf 13 | 14 | *.docx diff=astextplain 15 | *.DOCX diff=astextplain 16 | *.jar binary -------------------------------------------------------------------------------- /lib/util/Json.ts: -------------------------------------------------------------------------------- 1 | export type Json = boolean | number | string | null | JsonArray | JsonMap; 2 | export interface JsonMap { [key: string]: Json; } 3 | export interface JsonArray extends Array {} 4 | 5 | export type JsonSerializable = { 6 | toJSON(): JsonLike 7 | }; 8 | 9 | export type JsonLike = boolean | number | string | null | JsonSerializable | Array | { [P in any]: JsonLike }; 10 | -------------------------------------------------------------------------------- /lib/util/uuid.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from 'uuid'; 2 | 3 | const uuid = v4 as uuid; 4 | 5 | // eslint-disable-next-line @typescript-eslint/naming-convention,@typescript-eslint/no-redeclare 6 | interface uuid { 7 | /** 8 | * Generates a new Universally Unique Identifier (UUID) version 4. 9 | * 10 | * @return A generated version 4 UUID. 11 | */ 12 | (): string 13 | } 14 | export { uuid }; 15 | -------------------------------------------------------------------------------- /lib/util/hmac.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | /** 4 | * Calculates a Keyed-Hash Message Authentication Code (HMAC) from a message and a key. 5 | * 6 | * @param message 7 | * @param key 8 | * @return 9 | */ 10 | export function hmac(message: string, key: string): Promise { 11 | return Promise.resolve( 12 | crypto.createHmac('sha1', key) 13 | .update(message) 14 | .digest('hex'), 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /lib/query/index.ts: -------------------------------------------------------------------------------- 1 | export { Builder } from './Builder'; 2 | export { Condition } from './Condition'; 3 | export { FilterObject, Filter } from './Filter'; 4 | export { Node } from './Node'; 5 | export { Operator } from './Operator'; 6 | export { 7 | CompleteCallback, 8 | FailCallback, 9 | flatArgs, 10 | ResultListCallback, 11 | ResultOptions, 12 | CountCallback, 13 | SingleResultCallback, 14 | Query, 15 | } from './Query'; 16 | -------------------------------------------------------------------------------- /lib/util/enumerable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This decorator modifies the enumerable flag of an class member 3 | * @param value - the enumerable value of the property descriptor 4 | */ 5 | export function enumerable(value: boolean) { 6 | return function decorate( 7 | target: any, 8 | propertyKey: string, 9 | descriptor: PropertyDescriptor, 10 | ) { 11 | // eslint-disable-next-line no-param-reassign 12 | descriptor.enumerable = value; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /lib/model/index.ts: -------------------------------------------------------------------------------- 1 | import * as binding from '../binding'; 2 | 3 | /** 4 | * Users are representations of people using the app. 5 | */ 6 | export interface User extends binding.User {} 7 | 8 | /** 9 | * Roles are aggregations of multiple Users with a given purpose. 10 | */ 11 | export interface Role extends binding.Role {} 12 | 13 | /** 14 | * Devices are connected to the app to be contactable. 15 | */ 16 | export interface Device extends binding.Entity {} 17 | 18 | -------------------------------------------------------------------------------- /lib/connector/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | Message, OAuthMessage, ProgressListener, MessageSpec, StatusCode, RestSpecification, 3 | } from './Message'; 4 | export { 5 | ResponseBodyType, RequestBodyType, RequestBody, Response, Connector, Request, Receiver, 6 | } from './Connector'; 7 | export { FetchConnector } from './FetchConnector'; 8 | export { XMLHttpConnector } from './XMLHttpConnector'; 9 | export { IFrameConnector } from './IFrameConnector'; 10 | export { NodeConnector } from './NodeConnector'; 11 | -------------------------------------------------------------------------------- /lib/error/IllegalEntityError.ts: -------------------------------------------------------------------------------- 1 | import { PersistentError } from './PersistentError'; 2 | import { Entity } from '../binding'; 3 | 4 | export class IllegalEntityError extends PersistentError { 5 | /** 6 | * The entity which cause the error 7 | */ 8 | public entity: Entity; 9 | 10 | /** 11 | * @param entity - The entity which cause the error 12 | */ 13 | constructor(entity: Entity) { 14 | super(`Entity ${entity} is not a valid entity`); 15 | this.entity = entity; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/error/EntityExistsError.ts: -------------------------------------------------------------------------------- 1 | import { PersistentError } from './PersistentError'; 2 | import { Entity } from '../binding'; 3 | 4 | export class EntityExistsError extends PersistentError { 5 | /** 6 | * The entity which cause the error 7 | */ 8 | public entity: Entity; 9 | 10 | /** 11 | * @param entity - The entity which cause the error 12 | */ 13 | constructor(entity: Entity) { 14 | super(`The entity ${entity} is managed by a different db.`); 15 | this.entity = entity; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /spec-ts/types.d.ts: -------------------------------------------------------------------------------- 1 | import { binding } from 'baqend'; 2 | 3 | declare module 'baqend' { 4 | // eslint-disable-next-line @typescript-eslint/naming-convention 5 | interface baqend { 6 | Test:binding.EntityFactory; 7 | } 8 | 9 | // eslint-disable-next-line no-shadow 10 | namespace model { 11 | export interface User extends binding.Entity { 12 | customUserProp: string; 13 | } 14 | 15 | export interface Test extends binding.Entity { 16 | myProp: string; 17 | file: binding.File; 18 | } 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /lib/util/index.ts: -------------------------------------------------------------------------------- 1 | export { isNode } from './is-node'; 2 | export { atob } from './atob'; 3 | export { hmac } from './hmac'; 4 | export { Lockable } from './Lockable'; 5 | export { uuid } from './uuid'; 6 | export { 7 | JsonMap, Json, JsonLike, JsonArray, JsonSerializable, 8 | } from './Json'; 9 | export { Class } from './Class'; 10 | export { deprecated } from './deprecated'; 11 | export { trailingSlashIt } from './trailingSlashIt'; 12 | export { openWindow, OpenWindowHandler } from './openWindow'; 13 | export { Base64 } from './Base64'; 14 | export { MFAResponse } from './Mfa'; 15 | -------------------------------------------------------------------------------- /esm/lib/index.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/extensions 2 | import pkg from '../../commonjs/lib/index.js'; 3 | 4 | export const { 5 | connector, 6 | error, 7 | message, 8 | metamodel, 9 | util, 10 | intersection, 11 | caching, 12 | query, 13 | partialupdate, 14 | model, 15 | EntityManagerFactory, 16 | ConnectData, 17 | EntityManager, 18 | Acl, 19 | GeoPoint, 20 | baqend, 21 | db, 22 | connect, 23 | configure, 24 | Set, 25 | Map, 26 | List, 27 | } = pkg; 28 | 29 | // eslint-disable-next-line import/no-default-export 30 | export default db; 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "es6", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "rootDir": "./", 8 | "allowJs": false, 9 | "checkJs": false, 10 | "inlineSourceMap": true, 11 | "noImplicitAny": true, 12 | "experimentalDecorators": true, 13 | "typeRoots": [ 14 | "./node_modules/@types" 15 | ], 16 | "baseUrl": ".", 17 | "paths": { 18 | "baqend": ["."] 19 | } 20 | }, 21 | "exclude": [ 22 | "node_modules", 23 | "commonjs", 24 | "dist", 25 | "esm" 26 | ], 27 | } 28 | -------------------------------------------------------------------------------- /lib/intersection/index.ts: -------------------------------------------------------------------------------- 1 | export { Metadata, MetadataState, ManagedState } from './Metadata'; 2 | export { Permission, TrustedEntity } from './Permission'; 3 | export { Validator } from './Validator'; 4 | export { ValidationResult } from './ValidationResult'; 5 | export { Code } from './Code'; 6 | export { Modules } from './Modules'; 7 | export { Logger } from './Logger'; 8 | export { PushMessage, PushMessageOptions } from './PushMessage'; 9 | export { TokenStorageFactory, TokenStorage, TokenData } from './TokenStorage'; 10 | export { GlobalStorage } from './GlobalStorage'; 11 | export { WebStorage } from './WebStorage'; 12 | -------------------------------------------------------------------------------- /lib/binding/Accessor.ts: -------------------------------------------------------------------------------- 1 | import { Managed } from './Managed'; 2 | import { Attribute } from '../metamodel'; 3 | 4 | export class Accessor { 5 | /** 6 | * @param object 7 | * @param attribute 8 | * @return 9 | */ 10 | getValue(object: Managed, attribute: Attribute): T | null { 11 | return object[attribute.name] as T | null; 12 | } 13 | 14 | /** 15 | * @param object 16 | * @param attribute 17 | * @param value 18 | */ 19 | setValue(object: Managed, attribute: Attribute, value: T): void { 20 | // eslint-disable-next-line no-param-reassign 21 | object[attribute.name] = value; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/intersection/ValidationResult.ts: -------------------------------------------------------------------------------- 1 | import { JsonMap } from '../util/Json'; 2 | import { Validator } from './Validator'; 3 | 4 | export class ValidationResult { 5 | public fields: { [property: string]: Validator } = {}; 6 | 7 | /** 8 | * Indicates if all fields are valid 9 | * @return true if all fields are valid 10 | */ 11 | get isValid(): boolean { 12 | return Object.keys(this.fields).every((key) => this.fields[key].isValid); 13 | } 14 | 15 | toJSON(): JsonMap { 16 | const json: JsonMap = {}; 17 | Object.keys(this.fields).forEach((key) => { 18 | json[key] = this.fields[key].toJSON(); 19 | }); 20 | return json; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/binding/index.ts: -------------------------------------------------------------------------------- 1 | export { Accessor } from './Accessor'; 2 | export { Enhancer } from './Enhancer'; 3 | export { Factory, InstanceFactory } from './Factory'; 4 | export { ManagedFactory } from './ManagedFactory'; 5 | export { EntityFactory } from './EntityFactory'; 6 | export { LoginOption, UserFactory, OAuthOptions } from './UserFactory'; 7 | export { DeviceFactory } from './DeviceFactory'; 8 | export { RootFolderMetadata, FileFactory } from './FileFactory'; 9 | export { Managed } from './Managed'; 10 | export { Entity } from './Entity'; 11 | export { Role } from './Role'; 12 | export { User } from './User'; 13 | export { 14 | FileMetadata, File, FileOptions, FileData, FileIdentifiers, 15 | } from './File'; 16 | -------------------------------------------------------------------------------- /spec/globalDB.spec.js: -------------------------------------------------------------------------------- 1 | if (typeof module !== 'undefined') { 2 | require('./node'); 3 | } 4 | 5 | describe('Test Global DB', function () { 6 | before(async function () { 7 | await helper.ensureGlobalConnected(); 8 | }); 9 | 10 | it('should export global DB', function () { 11 | expect(DB).be.ok; 12 | expect(DB).instanceof(DB.EntityManager); 13 | }); 14 | 15 | it('should allow to add an callback to global DB object', function () { 16 | return DB.ready(function (localDb) { 17 | expect(localDb).equal(DB); 18 | expect(localDb).instanceof(DB.EntityManager); 19 | }); 20 | }); 21 | 22 | it('should only allow one connect call', async function () { 23 | expect(function () { 24 | DB.connect(env.TEST_SERVER); 25 | }).throw(Error); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /lib/util/hmac-browser.ts: -------------------------------------------------------------------------------- 1 | export function hmac(message: string, key: string): Promise { 2 | const encoder = new TextEncoder(); 3 | 4 | return Promise.resolve( 5 | crypto.subtle.importKey( 6 | 'raw', // raw format of the key - should be Uint8Array 7 | encoder.encode(key), 8 | { // algorithm details 9 | name: 'HMAC', 10 | hash: { name: 'SHA-1' }, 11 | }, 12 | false, // export = false 13 | ['sign', 'verify'], // what this key can do 14 | ), 15 | ) 16 | .then((cryptoKey) => crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(message))) 17 | .then((signature) => { 18 | const byteArray = new Uint8Array(signature); 19 | return byteArray.reduce((token, x) => token + x.toString(16).padStart(2, '0'), ''); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /lib/metamodel/CollectionAttribute.ts: -------------------------------------------------------------------------------- 1 | import { CollectionType, PluralAttribute } from './PluralAttribute'; 2 | import { Class } from '../util'; 3 | import { Type } from './Type'; 4 | 5 | export abstract class CollectionAttribute extends PluralAttribute { 6 | /** 7 | * @inheritDoc 8 | */ 9 | get collectionType() { 10 | return CollectionType.COLLECTION; 11 | } 12 | 13 | /** 14 | * @param name - the name of the attribute 15 | * @param typeConstructor - The collection constructor of the attribute 16 | * @param elementType - The element type of the collection 17 | */ 18 | protected constructor(name: string, typeConstructor: Class, elementType: Type) { 19 | super(name, typeConstructor, elementType); 20 | super(name, typeConstructor, elementType); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/metamodel/index.ts: -------------------------------------------------------------------------------- 1 | export { Metamodel } from './Metamodel'; 2 | export { DbIndex } from './DbIndex'; 3 | export { Type, PersistenceType } from './Type'; 4 | export { SingularAttribute } from './SingularAttribute'; 5 | export { SetAttribute } from './SetAttribute'; 6 | export { PluralAttribute, CollectionType } from './PluralAttribute'; 7 | export { ModelBuilder } from './ModelBuilder'; 8 | export { MapAttribute } from './MapAttribute'; 9 | export { ManagedType } from './ManagedType'; 10 | export { ListAttribute } from './ListAttribute'; 11 | export { EntityType } from './EntityType'; 12 | export { EmbeddableType } from './EmbeddableType'; 13 | export { CollectionAttribute } from './CollectionAttribute'; 14 | export { BasicType } from './BasicType'; 15 | export { Attribute, PersistentAttributeType } from './Attribute'; 16 | -------------------------------------------------------------------------------- /lib/intersection/GlobalStorage.ts: -------------------------------------------------------------------------------- 1 | import { TokenStorage } from './TokenStorage'; 2 | 3 | export class GlobalStorage extends TokenStorage { 4 | private static tokens: { [origin: string]: string } = {}; 5 | 6 | /** 7 | * Creates a global token storage instance for the given origin 8 | * A global token storage use a global variable to store the actual origin tokens 9 | * @param origin 10 | * @return 11 | */ 12 | static create(origin: string): Promise { 13 | return Promise.resolve(new GlobalStorage(origin, GlobalStorage.tokens[origin])); 14 | } 15 | 16 | /** 17 | * @inheritDoc 18 | */ 19 | saveToken(origin: string, token: string, temporary: boolean) { 20 | if (!temporary) { 21 | if (token) { 22 | GlobalStorage.tokens[origin] = token; 23 | } else { 24 | delete GlobalStorage.tokens[origin]; 25 | } 26 | } 27 | } 28 | } 29 | 30 | TokenStorage.GLOBAL = GlobalStorage; 31 | -------------------------------------------------------------------------------- /web-test-runner.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | concurrency: 10, 3 | nodeResolve: true, 4 | watch: true, 5 | files: 'spec/**/*.spec.js', 6 | // manual: true, 7 | // http2: true, 8 | testFramework: { 9 | config: { 10 | timeout: 30000, 11 | }, 12 | }, 13 | testRunnerHtml: testFramework => 14 | ` 15 | 16 | 17 | 18 | 19 | 25 | 26 | 27 | 28 | 29 | ` 30 | }; 31 | -------------------------------------------------------------------------------- /lib/util/atob-rn.ts: -------------------------------------------------------------------------------- 1 | // This implementation is based on https://github.com/mathiasbynens/base64/blob/master/src/base64.js 2 | const TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; 3 | 4 | export function atob(input: string): string { 5 | const str = input.length % 4 === 0 ? input.replace(/==?$/, '') : input; 6 | const { length } = str; 7 | 8 | let bitCounter = 0; 9 | let bitStorage = 0; 10 | let output = ''; 11 | for (let position = 0; position < length; position += 1) { 12 | const buffer = TABLE.indexOf(str.charAt(position)); 13 | const isModFour = bitCounter % 4; 14 | bitStorage = isModFour ? (bitStorage * 64) + buffer : buffer; 15 | bitCounter += 1; 16 | 17 | // Unless this is the first of a group of 4 characters… 18 | if (!isModFour) { 19 | // …convert the first 8 bits to a single ASCII character. 20 | // eslint-disable-next-line no-bitwise 21 | output += String.fromCharCode(0xFF & (bitStorage >> ((-2 * bitCounter) & 6))); 22 | } 23 | } 24 | 25 | return output; 26 | } 27 | -------------------------------------------------------------------------------- /lib/partialupdate/EntityPartialUpdateBuilder.ts: -------------------------------------------------------------------------------- 1 | import { PartialUpdateBuilder } from './PartialUpdateBuilder'; 2 | import { JsonMap } from '../util'; 3 | import * as message from '../message'; 4 | import { Entity } from '../binding'; 5 | import { Metadata } from '../intersection'; 6 | 7 | export class EntityPartialUpdateBuilder extends PartialUpdateBuilder { 8 | /** 9 | * @param entity 10 | * @param operations 11 | */ 12 | constructor(public readonly entity: T, operations: JsonMap) { 13 | super(operations); 14 | } 15 | 16 | /** 17 | * @inheritDoc 18 | */ 19 | execute(): Promise { 20 | const state = Metadata.get(this.entity); 21 | const body = JSON.stringify(this); 22 | const msg = new message.UpdatePartially(state.bucket, state.key!, body); 23 | 24 | return state.withLock(() => ( 25 | state.db.send(msg).then((response) => { 26 | // Update the entity’s values 27 | state.type.fromJsonValue(state, response.entity, this.entity, { persisting: true }); 28 | return this.entity; 29 | }) 30 | )); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Baqend GmbH 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import { db } from './baqend'; 2 | 3 | export * as binding from './binding'; 4 | export * as connector from './connector'; 5 | export * as error from './error'; 6 | export * as message from './message'; 7 | export * as metamodel from './metamodel'; 8 | export * as util from './util'; 9 | export * as intersection from './intersection'; 10 | export * as caching from './caching'; 11 | export * as query from './query'; 12 | export * as partialupdate from './partialupdate'; 13 | export * as model from './model'; 14 | 15 | export { EntityManagerFactory, ConnectData } from './EntityManagerFactory'; 16 | export { EntityManager } from './EntityManager'; 17 | export { Acl } from './Acl'; 18 | export { GeoPoint } from './GeoPoint'; 19 | export { db, baqend } from './baqend'; 20 | export { connect, configure } from './deperecated-exports'; 21 | 22 | const SetType = Set; 23 | const MapType = Map; 24 | const ListType = Array; 25 | 26 | export { SetType as Set, ListType as List, MapType as Map }; 27 | 28 | // Use one global default export of this module 29 | // eslint-disable-next-line import/no-default-export 30 | export default db; 31 | -------------------------------------------------------------------------------- /web-test-runner.pw.config.mjs: -------------------------------------------------------------------------------- 1 | import { playwrightLauncher } from '@web/test-runner-playwright'; 2 | import { defaultReporter } from '@web/test-runner'; 3 | import { junitReporter } from '@web/test-runner-junit-reporter'; 4 | 5 | import localConfig from './web-test-runner.config.mjs'; 6 | 7 | export default { 8 | ...localConfig, 9 | name: 'playwright', 10 | watch: false, 11 | reporters: [ 12 | // use the default reporter only for reporting test progress 13 | defaultReporter({ reportTestResults: true, reportTestProgress: true }), 14 | // use another reporter to report test results 15 | junitReporter({ 16 | outputPath: './playwright-result.xml', 17 | reportLogs: true, // default `false` 18 | }), 19 | ], 20 | concurrentBrowsers: 1, 21 | // amount of test files to execute concurrently in a browser. the default value is based 22 | // on amount of available CPUs locally which is irrelevant when testing remotely 23 | concurrency: 1, 24 | browsers: [ 25 | playwrightLauncher({ product: 'chromium' }), 26 | playwrightLauncher({ product: 'firefox' }), 27 | playwrightLauncher({ product: 'webkit' }), 28 | ].filter(b => !process.env.BROWSER || b.name.startsWith(process.env.BROWSER)), 29 | }; -------------------------------------------------------------------------------- /lib/query/Operator.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '../binding'; 2 | import { Node } from './Node'; 3 | import type { EntityManager } from '../EntityManager'; 4 | import { Class } from '../util'; 5 | 6 | /** 7 | * An Operator saves the state of a combined query 8 | */ 9 | export class Operator extends Node { 10 | /** 11 | * The operator used to join the child queries 12 | */ 13 | public readonly operator: string; 14 | 15 | /** 16 | * The child Node of this query, it is always one 17 | */ 18 | public readonly childes: Node[]; 19 | 20 | /** 21 | * @param entityManager The owning entity manager of this query 22 | * @param resultClass The query result class 23 | * @param operator The operator used to join the childes 24 | * @param childes The childes to join 25 | */ 26 | constructor(entityManager: EntityManager, resultClass: Class, operator: string, childes: Node[]) { 27 | super(entityManager, resultClass); 28 | this.operator = operator; 29 | this.childes = childes; 30 | } 31 | 32 | toJSON(): { [operator: string]: Node[] } { 33 | const json: { [operator: string]: Node[] } = {}; 34 | json[this.operator] = this.childes; 35 | return json; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /orestes-js.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /lib/metamodel/EmbeddableType.ts: -------------------------------------------------------------------------------- 1 | import { ManagedType } from './ManagedType'; 2 | import { PersistenceType } from './Type'; 3 | import { Managed, ManagedFactory } from '../binding'; 4 | import { Class, Json } from '../util'; 5 | import type { EntityManager } from '../EntityManager'; 6 | import { ManagedState } from '../intersection'; 7 | 8 | export class EmbeddableType extends ManagedType { 9 | /** 10 | * @inheritDoc 11 | */ 12 | get persistenceType() { 13 | return PersistenceType.EMBEDDABLE; 14 | } 15 | 16 | /** 17 | * @inheritDoc 18 | */ 19 | createProxyClass(): Class { 20 | return this.enhancer!.createProxy(Managed); 21 | } 22 | 23 | /** 24 | * @inheritDoc 25 | */ 26 | createObjectFactory(db: EntityManager): ManagedFactory { 27 | return ManagedFactory.create(this, db); 28 | } 29 | 30 | /** 31 | * @inheritDoc 32 | */ 33 | fromJsonValue(state: ManagedState, jsonObject: Json, currentObject: T | null, 34 | options: { onlyMetadata?: boolean, persisting: boolean }) { 35 | let obj = currentObject; 36 | 37 | if (jsonObject) { 38 | if (!(obj instanceof this.typeConstructor)) { 39 | obj = this.create(); 40 | } 41 | } 42 | 43 | return super.fromJsonValue(state, jsonObject, obj, options); 44 | } 45 | 46 | toString() { 47 | return `EmbeddableType(${this.ref})`; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | { "name": "main" }, 4 | { "name": "prerelease", "channel": "pre", "prerelease": true } 5 | ], 6 | "plugins": [ 7 | [ "@semantic-release/commit-analyzer", 8 | { 9 | "preset": "conventionalcommits" 10 | } 11 | ], 12 | [ 13 | "@semantic-release/release-notes-generator", 14 | { 15 | "linkCompare": false, 16 | "linkReferences": false, 17 | "preset": "conventionalcommits" 18 | } 19 | ], 20 | [ 21 | "@semantic-release/changelog", 22 | { 23 | "changelogFile": "CHANGELOG.md" 24 | } 25 | ], 26 | "@semantic-release/npm", 27 | "@semantic-release/gitlab", 28 | [ 29 | "@semantic-release/exec", 30 | { 31 | "verifyConditionsCmd": "bash ./scripts/publish.sh --action verify --base-path ${config.basePath} --project ${config.project}", 32 | "publishCmd": "bash ./scripts/publish.sh --action publish --base-path ${config.basePath} --project ${config.project} --version ${nextRelease.version} --channel ${nextRelease.channel || 'latest'} --assets ${config.assets.join(',')}", 33 | "project": "js-sdk", 34 | "assets": ["dist", "doc"], 35 | "basePath": "s3://baqend-website" 36 | } 37 | ], 38 | [ 39 | "@semantic-release/git", 40 | { 41 | "assets": ["package.json", "package-lock.json", "CHANGELOG.md"], 42 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 43 | } 44 | ] 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /lib/util/openWindow.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An window open handler which can be used to open up the OAuth Window 3 | * @param url Thw URl which should be opened in the Popup 4 | * @param options Additional window options 5 | * @param target The target of the window 6 | * @param title title of the window, if no target was set 7 | * @return An optional opened window handle which will be returned by the redirect based OAuth flow. 8 | * If null is returned it indicates that the open window have bee failed 9 | */ 10 | export type OpenWindowHandler = (url: string, options: { 11 | title: string, target?: string, [option: string]: string | number | undefined } 12 | ) => any | null; 13 | 14 | export const openWindow: OpenWindowHandler = (url: string, opt: { 15 | target?: string, title: string, [option: string]: string | number | undefined }) => { 16 | const { title, ...options } = opt; 17 | let { target } = opt; 18 | 19 | const str = Object.keys(options) 20 | .filter((key) => options[key] !== undefined) 21 | .map((key) => `${key}=${options[key]}`) 22 | .join(','); 23 | 24 | if (target === '_self') { 25 | // for app wrappers we need to open the system browser 26 | if (typeof document === 'undefined' || (document.URL.indexOf('http://') === -1 && document.URL.indexOf('https://') === -1)) { 27 | target = '_system'; 28 | } 29 | } 30 | 31 | if (typeof open !== 'undefined') { // eslint-disable-line no-restricted-globals 32 | return open(url, (target || title), str); // eslint-disable-line no-restricted-globals 33 | } 34 | 35 | return null; 36 | }; 37 | -------------------------------------------------------------------------------- /lib/binding/Role.ts: -------------------------------------------------------------------------------- 1 | import type * as model from '../model'; 2 | import { User } from './User'; 3 | import { Entity } from './Entity'; 4 | import { enumerable } from '../util/enumerable'; 5 | 6 | export class Role extends Entity { 7 | /** 8 | * A set of users which have this role 9 | */ 10 | public users: Set | null = null; 11 | 12 | /** 13 | * The name of the role 14 | */ 15 | public name: string | null = null; 16 | 17 | /** 18 | * Test if the given user has this role 19 | * @param user The user to check 20 | * @return true if the given user has this role, 21 | * otherwise false 22 | */ 23 | @enumerable(false) 24 | hasUser(user: model.User): boolean { 25 | return !!this.users && this.users.has(user); 26 | } 27 | 28 | /** 29 | * Add the given user to this role 30 | * @param user The user to add 31 | */ 32 | @enumerable(false) 33 | addUser(user: model.User): void { 34 | if (user instanceof User) { 35 | if (!this.users) { 36 | this.users = new Set(); 37 | } 38 | 39 | this.users.add(user); 40 | } else { 41 | throw new Error('Only user instances can be added to a role.'); 42 | } 43 | } 44 | 45 | /** 46 | * Remove the given user from this role 47 | * @param user The user to remove 48 | */ 49 | @enumerable(false) 50 | removeUser(user: model.User): void { 51 | if (user instanceof User) { 52 | if (this.users) { 53 | this.users.delete(user); 54 | } 55 | } else { 56 | throw new Error('Only user instances can be removed from a role.'); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/intersection/WebStorage.ts: -------------------------------------------------------------------------------- 1 | import { TokenStorage } from './TokenStorage'; 2 | 3 | /** 4 | * @ignore 5 | */ 6 | export class WebStorage extends TokenStorage { 7 | static isAvailable() { 8 | try { 9 | // firefox throws an exception if cookies are disabled 10 | if (typeof localStorage === 'undefined') { 11 | return false; 12 | } 13 | 14 | localStorage.setItem('bq_webstorage_test', 'bq'); 15 | localStorage.removeItem('bq_webstorage_test'); 16 | return true; 17 | } catch (e) { 18 | return false; 19 | } 20 | } 21 | 22 | /** 23 | * Creates a global web storage instance for the given origin 24 | * A web token storage use the localStorage or sessionStorage to store the origin tokens 25 | * @param origin 26 | * @return 27 | */ 28 | static create(origin: string): Promise { 29 | let temporary = false; 30 | let token = localStorage.getItem(`BAT:${origin}`); 31 | 32 | if (!token && typeof sessionStorage !== 'undefined') { 33 | token = sessionStorage.getItem(`BAT:${origin}`); 34 | temporary = !!token; 35 | } 36 | 37 | return Promise.resolve(new WebStorage(origin, token, temporary)); 38 | } 39 | 40 | /** 41 | * @inheritDoc 42 | */ 43 | saveToken(origin: string, token: string | null, temporary: boolean) { 44 | const webStorage = temporary ? sessionStorage : localStorage; 45 | if (token) { 46 | webStorage.setItem(`BAT:${origin}`, token); 47 | } else { 48 | webStorage.removeItem(`BAT:${origin}`); 49 | } 50 | } 51 | } 52 | 53 | if (WebStorage.isAvailable()) { 54 | TokenStorage.WEB_STORAGE = WebStorage; 55 | } 56 | -------------------------------------------------------------------------------- /lib/metamodel/PluralAttribute.ts: -------------------------------------------------------------------------------- 1 | import { Class, Json } from '../util'; 2 | import { Type } from './Type'; 3 | import { Attribute, PersistentAttributeType } from './Attribute'; 4 | 5 | export enum CollectionType { 6 | COLLECTION = 0, 7 | LIST = 1, 8 | MAP = 2, 9 | SET = 3, 10 | } 11 | 12 | export abstract class PluralAttribute extends Attribute { 13 | public static readonly CollectionType = CollectionType; 14 | 15 | public elementType: Type; 16 | 17 | public typeConstructor: Class; 18 | 19 | /** 20 | * Returns the collection attribute type 21 | */ 22 | abstract get collectionType(): CollectionType; 23 | 24 | /** 25 | * @inheritDoc 26 | */ 27 | get persistentAttributeType() { 28 | return PersistentAttributeType.ELEMENT_COLLECTION; 29 | } 30 | 31 | /** 32 | * @param name - The attribute name 33 | * @param typeConstructor - The collection constructor of the attribute 34 | * @param elementType - The type of the elements of the attribute collection 35 | */ 36 | protected constructor(name: string, typeConstructor: Class, elementType: Type) { 37 | super(name); 38 | this.elementType = elementType; 39 | this.typeConstructor = typeConstructor; 40 | } 41 | 42 | /** 43 | * Retrieves a serialized string value of the given json which can be used as object keys 44 | * @param json The json of which the key should be retrieved 45 | * @return A serialized version of the json 46 | */ 47 | protected keyValue(json: Json): string { 48 | if (json && typeof json === 'object' && 'id' in json) { 49 | return String(json.id); 50 | } 51 | 52 | return String(json); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/query/Filter.ts: -------------------------------------------------------------------------------- 1 | import { Node } from './Node'; 2 | import { Condition } from './Condition'; 3 | import { Json } from '../util'; 4 | import type { Entity } from '../binding'; 5 | 6 | export type FilterObject = { [key: string]: NestedFilter | Json | Entity | Date }; 7 | export type NestedFilter = { [filter: string]: Json | Entity | Date }; 8 | 9 | /** 10 | * A Filter saves the state for a filtered query 11 | */ 12 | export interface Filter extends Node, Condition {} // mixin the condition implementation 13 | export class Filter extends Node { 14 | /** 15 | * The actual filters of this node 16 | */ 17 | public readonly filter: FilterObject = {}; 18 | 19 | /** 20 | * @inheritDoc 21 | */ 22 | addFilter(field: string | null, filter: string | null, value: any): Filter { 23 | if (field !== null) { 24 | if (typeof field !== 'string') { 25 | throw new Error('Field must be a string.'); 26 | } 27 | 28 | if (filter) { 29 | const currentFilter = this.filter[field]; 30 | let fieldFilter: NestedFilter; 31 | if (typeof currentFilter === 'object' && Object.getPrototypeOf(currentFilter) === Object.prototype) { 32 | fieldFilter = currentFilter as NestedFilter; 33 | } else { 34 | fieldFilter = {}; 35 | this.filter[field] = fieldFilter; 36 | } 37 | 38 | fieldFilter[filter] = value; 39 | } else { 40 | this.filter[field] = value; 41 | } 42 | } else { 43 | Object.assign(this.filter, value); 44 | } 45 | 46 | return this; 47 | } 48 | 49 | toJSON(): FilterObject { 50 | return this.filter; 51 | } 52 | } 53 | 54 | Object.assign(Filter.prototype, Condition); 55 | -------------------------------------------------------------------------------- /spec/geopoint.spec.js: -------------------------------------------------------------------------------- 1 | if (typeof module !== 'undefined') { 2 | require('./node'); 3 | } 4 | 5 | describe('Test GeoPoint', function () { 6 | it('should construct with an latitude and longitude argument', function () { 7 | var point = new DB.GeoPoint(56.5, 165.2); 8 | 9 | expect(point.latitude).equals(56.5); 10 | expect(point.longitude).equals(165.2); 11 | }); 12 | 13 | it('should construct with an array argument', function () { 14 | var point = new DB.GeoPoint([-36.5, -92.3]); 15 | 16 | expect(point.latitude).equals(-36.5); 17 | expect(point.longitude).equals(-92.3); 18 | }); 19 | 20 | it('should construct with an geolike object argument', function () { 21 | var point = new DB.GeoPoint({ latitude: 90, longitude: -180.0 }); 22 | 23 | expect(point.latitude).equals(90); 24 | expect(point.longitude).equals(-180.0); 25 | }); 26 | 27 | it('should construct from json argument', function () { 28 | var point1 = new DB.GeoPoint({ latitude: -90, longitude: 180.0 }); 29 | var point2 = new DB.GeoPoint(point1.toJSON()); 30 | 31 | expect(point1).eql(point2); 32 | }); 33 | 34 | it('should compute distance', function () { 35 | var point1 = new DB.GeoPoint(53.5753, 10.0153); // Hamburg 36 | var point2 = new DB.GeoPoint(40.7143, -74.006); // New York 37 | var point3 = new DB.GeoPoint(-33.8679, 151.207); // Sydney 38 | var point4 = new DB.GeoPoint(51.5085, -0.1257); // London 39 | 40 | expect(point1.kilometersTo(point2)).within(6147 * 0.97, 6147 * 1.03); 41 | expect(point1.milesTo(point2)).within(3819 * 0.97, 3819 * 1.03); 42 | expect(point3.kilometersTo(point4)).within(16989 * 0.97, 16989 * 1.03); 43 | expect(point3.milesTo(point4)).within(10556 * 0.97, 10556 * 1.03); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /spec/connect.spec.js: -------------------------------------------------------------------------------- 1 | if (typeof module !== 'undefined') { 2 | require('./node'); 3 | } 4 | 5 | describe('Test Connect', function () { 6 | var user, origin; 7 | 8 | before(function () { 9 | user = helper.makeLogin(); 10 | var emf = new DB.EntityManagerFactory(env.TEST_SERVER); 11 | var em = emf.createEntityManager(); 12 | 13 | return em.ready().then(function () { 14 | return em.User.register(user, 'secret'); 15 | }); 16 | }); 17 | 18 | beforeEach(function () { 19 | var emf = new DB.EntityManagerFactory(env.TEST_SERVER); 20 | var db = emf.createEntityManager(true); 21 | 22 | return db.ready().then(function () { 23 | if (!db.User.me) { return db.User.login(user, 'secret'); } 24 | }); 25 | }); 26 | 27 | afterEach(function () { 28 | var emf = new DB.EntityManagerFactory(env.TEST_SERVER); 29 | var db = emf.createEntityManager(true); 30 | return db.ready().then(function () { 31 | return db.User.logout(); 32 | }); 33 | }); 34 | 35 | it('should resume a logged in session', function () { 36 | var emf = new DB.EntityManagerFactory(env.TEST_SERVER); 37 | var db = emf.createEntityManager(true); 38 | 39 | return db.ready().then(function (db) { 40 | expect(db.User.me).be.ok; 41 | expect(db.User.me.username).be.equal(user); 42 | expect(db.token).be.ok; 43 | }); 44 | }); 45 | 46 | it('should resume a logged in session with new connection', function () { 47 | DB.connector.Connector.connections = {}; 48 | 49 | var emf = new DB.EntityManagerFactory(env.TEST_SERVER); 50 | var db = emf.createEntityManager(true); 51 | 52 | return db.ready().then(function (db) { 53 | expect(db.User.me).be.ok; 54 | expect(db.User.me.username).be.equal(user); 55 | expect(db.token).be.ok; 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /lib/error/CommunicationError.ts: -------------------------------------------------------------------------------- 1 | import { PersistentError } from './PersistentError'; 2 | import { Json } from '../util'; 3 | import { Message, Response } from '../connector'; 4 | 5 | export class CommunicationError extends PersistentError { 6 | /** 7 | * The reason of the error 8 | */ 9 | public reason: string; 10 | 11 | /** 12 | * The error response status code of this error 13 | */ 14 | public status: number; 15 | 16 | /** 17 | * Additional Data to keep with the error 18 | */ 19 | public data?: Json; 20 | 21 | /** 22 | * @param httpMessage The http message which was send 23 | * @param response The received entity headers and content 24 | */ 25 | constructor(httpMessage: Message | null, response: Response) { 26 | const entity = response.entity || response.error || {}; 27 | const state = (response.status === 0 ? 'Request' : 'Response'); 28 | const message = entity.message 29 | || (httpMessage && `Handling the ${state} for ${httpMessage.request.method} ${httpMessage.request.path}`) 30 | || 'A communication error occurred.'; 31 | 32 | super(message, entity); 33 | 34 | this.name = entity.className || 'CommunicationError'; 35 | this.reason = entity.reason || 'Communication failed'; 36 | this.status = response.status; 37 | 38 | if (entity.data) { 39 | this.data = entity.data; 40 | } 41 | 42 | let cause = entity; 43 | while (cause && cause.stackTrace) { 44 | this.stack += `\nServerside Caused by: ${cause.className} ${cause.message}`; 45 | 46 | const { stackTrace } = cause; 47 | for (let i = 0; i < stackTrace.length; i += 1) { 48 | const el = stackTrace[i]; 49 | 50 | this.stack += `\n at ${el.className}.${el.methodName}`; 51 | this.stack += ` (${el.fileName}:${el.lineNumber})`; 52 | } 53 | 54 | cause = cause.cause; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/util/deprecated.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const alreadyWarned: { [signature: string]: boolean } = {}; 3 | export function deprecated(alternativeSignature: string) { 4 | return function decorateProperty( 5 | target: Object | string, 6 | name: string, 7 | descriptor: PropertyDescriptor = { 8 | writable: true, 9 | enumerable: false, 10 | configurable: true, 11 | }, 12 | ): PropertyDescriptor { 13 | const type = typeof target === 'string' ? target : target.constructor.name; 14 | const deprecatedSignature = `${type}.${name}`; 15 | const logWarning = () => { 16 | if (!alreadyWarned[deprecatedSignature]) { 17 | alreadyWarned[deprecatedSignature] = true; 18 | console.warn(`Usage of ${deprecatedSignature} is deprecated, use ${alternativeSignature} instead.`); 19 | } 20 | }; 21 | 22 | const deprecatedDescriptor: PropertyDescriptor = { 23 | enumerable: descriptor.enumerable, 24 | configurable: descriptor.configurable, 25 | }; 26 | 27 | if (descriptor.get || descriptor.set) { 28 | if (descriptor.get) { 29 | deprecatedDescriptor.get = function get() { 30 | logWarning(); 31 | return descriptor.get!.call(this); 32 | }; 33 | } 34 | 35 | if (descriptor.set) { 36 | deprecatedDescriptor.set = function set(value) { 37 | logWarning(); 38 | return descriptor.set!.call(this, value); 39 | }; 40 | } 41 | } else { 42 | let propertyValue = descriptor.value; 43 | 44 | deprecatedDescriptor.get = function get() { 45 | logWarning(); 46 | return propertyValue; 47 | }; 48 | 49 | if (descriptor.writable) { 50 | deprecatedDescriptor.set = function set(value) { 51 | logWarning(); 52 | propertyValue = value; 53 | }; 54 | } 55 | } 56 | 57 | return deprecatedDescriptor; 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /cli/download.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console,@typescript-eslint/no-use-before-define */ 2 | import path from 'path'; 3 | import { EntityManager } from 'baqend'; 4 | import * as account from './account'; 5 | import { AccountArgs } from './account'; 6 | import { 7 | ensureDir, writeFile, 8 | } from './helper'; 9 | 10 | export type DownloadArgs = { 11 | code: boolean, 12 | codeDir: string, 13 | }; 14 | 15 | export function download(args: DownloadArgs & AccountArgs) { 16 | return account.login(args).then((db) => { 17 | const promises: Promise[] = []; 18 | if (args.code) { 19 | promises.push(downloadCode(db, args.codeDir)); 20 | } 21 | return Promise.all(promises); 22 | }); 23 | } 24 | 25 | /** 26 | * Download all Baqend code. 27 | * 28 | * @param db The entity manager to use. 29 | * @param codePath The path where code should be downloaded to. 30 | * @return Resolves when downloading has been finished. 31 | */ 32 | function downloadCode(db: EntityManager, codePath: string): Promise { 33 | return ensureDir(codePath) 34 | .then(() => db.code.loadModules()) 35 | .then((modules) => Promise.all(modules.map((module) => downloadCodeModule(db, module, codePath)))); 36 | } 37 | 38 | /** 39 | * Downloads a single code module. 40 | * 41 | * @param {EntityManager} db The entity manager to use. 42 | * @param {string} module The module to download. 43 | * @param {string} codePath The path where code should be downloaded to. 44 | * @return {Promise} Resolves when downloading has been finished. 45 | */ 46 | function downloadCodeModule(db: EntityManager, module: string, codePath: string): Promise { 47 | const moduleName = module.replace(/^\/code\//, '').replace(/\/module$/, ''); 48 | const fileName = `${moduleName}.js`; 49 | const filePath = path.join(codePath, fileName); 50 | 51 | return db.code.loadCode(moduleName, 'module', false) 52 | .then((file) => writeFile(filePath, file, 'utf-8')) 53 | .then(() => console.log(`Module ${moduleName} downloaded.`)); 54 | } 55 | -------------------------------------------------------------------------------- /cli/helper.ts: -------------------------------------------------------------------------------- 1 | import readline from 'readline'; 2 | import { Writable } from 'stream'; 3 | import { promisify } from 'util'; 4 | import * as fs from 'fs'; 5 | 6 | export const readDir = promisify(fs.readdir); 7 | export const writeFile = promisify(fs.writeFile); 8 | export const readFile = promisify(fs.readFile); 9 | export const mkdir = promisify(fs.mkdir); 10 | 11 | export const nativeNamespaces = ['logs', 'speedKit', 'rum']; 12 | 13 | /** 14 | * Returns the stats for the given path 15 | * @param path 16 | */ 17 | export function stat(path: string) : Promise { 18 | return new Promise((resolve, reject) => { 19 | fs.stat(path, (err, st) => { 20 | if (!err) { 21 | resolve(st); 22 | } else if (err.code === 'ENOENT') { 23 | resolve(null); 24 | } else { 25 | reject(err); 26 | } 27 | }); 28 | }); 29 | } 30 | 31 | /** 32 | * Indicates if the given path is a directory 33 | * @param path 34 | */ 35 | export function isDir(path: string) : Promise { 36 | return stat(path).then((s) => !!s && s.isDirectory()); 37 | } 38 | 39 | /** 40 | * Indicates if the given path is a file 41 | * @param path 42 | */ 43 | export function isFile(path: string) : Promise { 44 | return stat(path).then((s) => !!s && s.isFile()); 45 | } 46 | 47 | /** 48 | * Creates a direcotry or ensures that it exists. 49 | * 50 | * @param {string} dir The path where a directory should exist. 51 | * @return {Promise} Resolves when the given directory is existing. 52 | */ 53 | export function ensureDir(dir: string): Promise { 54 | return isDir(dir).then((directory) => { 55 | if (!directory) { 56 | return mkdir(dir, { recursive: true }).then(() => {}); 57 | } 58 | return undefined; 59 | }); 60 | } 61 | 62 | export function isNativeClassNamespace(className: string): boolean { 63 | const [namespace] = className.split('.'); 64 | return nativeNamespaces.includes(namespace); 65 | } 66 | 67 | export function readModuleFile(path: string): Promise { 68 | const filename = require.resolve(path); 69 | return readFile(filename, 'utf8'); 70 | } 71 | -------------------------------------------------------------------------------- /lib/deperecated-exports.ts: -------------------------------------------------------------------------------- 1 | import * as binding from './binding'; 2 | import * as connector from './connector'; 3 | import * as error from './error'; 4 | import * as message from './message'; 5 | import * as util from './util'; 6 | import * as caching from './caching'; 7 | import * as query from './query'; 8 | import * as partialupdate from './partialupdate'; 9 | import * as intersection from './intersection'; 10 | import * as metamodel from './metamodel'; 11 | 12 | import { 13 | Permission, Metadata, TokenStorage, Validator, PushMessage, Code, Modules, Logger, 14 | } from './intersection'; 15 | import { Metamodel } from './metamodel'; 16 | import { EntityManagerFactory } from './EntityManagerFactory'; 17 | import { EntityManager } from './EntityManager'; 18 | import { Acl } from './Acl'; 19 | 20 | import { db } from './baqend'; 21 | import { deprecated } from './util'; 22 | 23 | function deprecateExports(target: Object, targetName: string, newImportSignature: string, exports: { 24 | [exported: string]: any 25 | }) { 26 | Object.keys(exports).forEach((exported) => { 27 | const decorate = deprecated(newImportSignature.replace('$export', exported)); 28 | Object.defineProperty(target, exported, decorate(targetName, exported, { 29 | get(): any { 30 | return (exports as any)[exported]; 31 | }, 32 | })); 33 | }); 34 | } 35 | 36 | deprecateExports(util, 'util', 'intersection.$export', { 37 | Permission, Metadata, TokenStorage, Validator, PushMessage, Code, Modules, Logger, 38 | }); 39 | 40 | deprecateExports(EntityManager.prototype, 'db', 'import { $export } from \'baqend\'', { 41 | db, 42 | binding, 43 | connector, 44 | error, 45 | message, 46 | util, 47 | caching, 48 | query, 49 | partialupdate, 50 | intersection, 51 | 52 | EntityManagerFactory, 53 | EntityManager, 54 | Acl, 55 | }); 56 | 57 | deprecateExports(Metamodel.prototype, 'metamodel', 'import { metamodel } from \'baqend\';', metamodel); 58 | 59 | export function configure() { 60 | throw new Error('Please use Baqend.db.configure() or import { db } from \'baqend\' instead.'); 61 | } 62 | 63 | export function connect() { 64 | throw new Error('Please use Baqend.db.connect() or import { db } from \'baqend\' instead.'); 65 | } 66 | -------------------------------------------------------------------------------- /lib/binding/User.ts: -------------------------------------------------------------------------------- 1 | import { enumerable } from '../util/enumerable'; 2 | import { Entity } from './Entity'; 3 | import type * as model from '../model'; 4 | import { Class, JsonMap } from '../util'; 5 | 6 | export class User extends Entity { 7 | /** 8 | * The users username or email address 9 | */ 10 | public username? : string | null; 11 | 12 | /** 13 | * Indicates if the user is currently inactive, which disallow user login 14 | */ 15 | public inactive? : boolean | null; 16 | 17 | /** 18 | * Change the password of the given user 19 | * 20 | * @param currentPassword Current password of the user 21 | * @param password New password of the user 22 | * @param doneCallback Called when the operation succeed. 23 | * @param failCallback Called when the operation failed. 24 | * @return 25 | */ 26 | @enumerable(false) 27 | newPassword(currentPassword: string, password: string, doneCallback?: any, failCallback?: any): Promise { 28 | return this._metadata.db.newPassword(this.username!!, currentPassword, password).then(doneCallback, failCallback); 29 | } 30 | 31 | /** 32 | * Change the username of the current user 33 | * 34 | * @param newUsername New username for the current user 35 | * @param password The password of the current user 36 | * @param doneCallback Called when the operation succeed. 37 | * @param failCallback Called when the operation failed. 38 | * @return 39 | */ 40 | @enumerable(false) 41 | changeUsername(newUsername: string, password: string, doneCallback?: any, failCallback?: any): Promise { 42 | return this._metadata.db.changeUsername(this.username!!, newUsername, password).then(doneCallback, failCallback); 43 | } 44 | 45 | /** 46 | * Requests a perpetual token for the user 47 | * 48 | * Only users with the admin role are allowed to request an API token. 49 | * 50 | * @param doneCallback Called when the operation succeed. 51 | * @param failCallback Called when the operation failed. 52 | * @return 53 | */ 54 | @enumerable(false) 55 | requestAPIToken(doneCallback?: any, failCallback?: any): Promise { 56 | return this._metadata.db.requestAPIToken(this.constructor as Class, this) 57 | .then(doneCallback, failCallback); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/baqend.ts: -------------------------------------------------------------------------------- 1 | import { EntityManagerFactory } from './EntityManagerFactory'; 2 | import { EntityManager } from './EntityManager'; 3 | import { TokenStorage, TokenStorageFactory } from './intersection'; 4 | 5 | // eslint-disable-next-line @typescript-eslint/naming-convention 6 | export interface baqend extends EntityManager { 7 | /** 8 | * Configures the DB with additional config options 9 | * @param options The additional configuration options 10 | * @param [options.tokenStorage] The tokenStorage which should be used by this emf 11 | * @param [options.tokenStorageFactory] The tokenStorage factory implementation which should 12 | * be used for token storage 13 | * @param [options.staleness=60] The maximum staleness of objects that are acceptable while reading cached 14 | * data, 0 to always bypass the browser cache 15 | */ 16 | configure(options: { tokenStorage?: TokenStorage, tokenStorageFactory?: TokenStorageFactory, staleness?: number }): 17 | this; 18 | 19 | /** 20 | * Connects the DB with the server and calls the callback on success 21 | * @param hostOrApp The host or the app name to connect with 22 | * @param [secure=false] true To use a secure connection 23 | * @param doneCallback The callback, called when a connection is established and the 24 | * SDK is ready to use 25 | * @param failCallback When an error occurred while initializing the SDK 26 | * @return A initialized EntityManager 27 | */ 28 | connect(hostOrApp: string, secure?: boolean, doneCallback?: any, failCallback?: any): Promise; 29 | } 30 | 31 | export const db = (() => { 32 | const emf = new EntityManagerFactory(); 33 | const bq = emf.createEntityManager(true); 34 | 35 | Object.assign(bq, { 36 | configure(this: baqend, options) { 37 | emf.configure(options); 38 | return this; 39 | }, 40 | 41 | connect(this: baqend, hostOrApp: string, secure?: boolean | Function, doneCallback?: any, failCallback?: any) { 42 | if (secure instanceof Function) { 43 | return this.connect(hostOrApp, undefined, secure, doneCallback); 44 | } 45 | 46 | emf.connect(hostOrApp, secure); 47 | return this.ready(doneCallback, failCallback); 48 | }, 49 | } as Partial); 50 | 51 | return bq as baqend; 52 | })(); 53 | -------------------------------------------------------------------------------- /lib/error/PersistentError.ts: -------------------------------------------------------------------------------- 1 | interface PersistentErrorConstructor { 2 | /** 3 | * Wraps the given error into a persistent error, if the error is not already an persistent error 4 | * @param error - The error to wrap 5 | */ 6 | of(error: Error): PersistentError; 7 | 8 | /** 9 | * @param message - a error message 10 | * @param cause - a optional cause of the error 11 | */ 12 | new(message: string | null, cause?: Error): PersistentError; 13 | } 14 | 15 | export interface PersistentError extends Error { 16 | /** 17 | * The name of the error 18 | */ 19 | name: string; 20 | 21 | /** 22 | * The error message 23 | */ 24 | message: string; 25 | 26 | /** 27 | * The error stack trace 28 | */ 29 | stack?: string; 30 | 31 | /** 32 | * The error cause 33 | */ 34 | cause?: Error; 35 | } 36 | 37 | // eslint-disable-next-line @typescript-eslint/no-redeclare 38 | export const PersistentError = (() => { 39 | function PersistentErrorConstructor(this: PersistentError, message: string | null, cause?: Error) { 40 | if (Object.prototype.hasOwnProperty.call(Error, 'captureStackTrace')) { 41 | Error.captureStackTrace(this, this.constructor); 42 | } else { 43 | this.stack = (new Error()).stack; 44 | } 45 | 46 | this.message = (message || 'An unexpected persistent error occurred.'); 47 | this.name = this.constructor.name; 48 | 49 | if (cause) { 50 | this.cause = cause; 51 | if (cause.stack) { 52 | this.stack += `\nCaused By: ${cause.stack}`; 53 | } 54 | } 55 | } 56 | 57 | // custom errors must be manually extended, since JS Errors can't be super called in a class hierarchy, 58 | // otherwise the super call destroys the origin 'this' reference 59 | PersistentErrorConstructor.prototype = Object.create(Error.prototype, { 60 | constructor: { 61 | value: PersistentErrorConstructor, 62 | writable: true, 63 | enumerable: false, 64 | configurable: true, 65 | }, 66 | }); 67 | 68 | return PersistentErrorConstructor as any as PersistentErrorConstructor; 69 | })(); 70 | 71 | PersistentError.of = function of(error: Error): PersistentError { 72 | if (error instanceof PersistentError) { 73 | return error; 74 | } 75 | 76 | return new PersistentError(null, error); 77 | }; 78 | -------------------------------------------------------------------------------- /lib/binding/ManagedFactory.ts: -------------------------------------------------------------------------------- 1 | import { Factory } from './Factory'; 2 | import type { EntityManager } from '../EntityManager'; 3 | import type { ManagedType } from '../metamodel'; 4 | import type { Managed } from './Managed'; 5 | import type { Json } from '../util'; 6 | import { Metadata } from '../intersection'; 7 | 8 | export class ManagedFactory extends Factory { 9 | /** 10 | * Creates a new ManagedFactory for the given type 11 | * @param managedType The metadata of type T 12 | * @param db The entity manager instance 13 | */ 14 | public static create(managedType: ManagedType, db: EntityManager): ManagedFactory { 15 | const factory: ManagedFactory = this.createFactory, T>(managedType.typeConstructor); 16 | 17 | factory.methods = factory.prototype; 18 | factory.managedType = managedType; 19 | factory.db = db; 20 | 21 | return factory; 22 | } 23 | 24 | /** 25 | * Methods that are added to object instances 26 | * This property is an alias for this factory type prototype 27 | * @name methods 28 | */ 29 | public methods: { [methodName: string]: any } = null as any; 30 | 31 | /** 32 | * The managed type of this factory 33 | */ 34 | public managedType: ManagedType = null as any; 35 | 36 | /** 37 | * The owning EntityManager where this factory belongs to 38 | */ 39 | public db: EntityManager = null as any; 40 | 41 | /** 42 | * Creates a new instance and sets the Managed Object to the given json 43 | * @param json 44 | * @return A new created instance of T 45 | */ 46 | fromJSON(json: Json): T { 47 | const instance = this.newInstance(); 48 | return this.managedType.fromJsonValue(Metadata.create(this.managedType, this.db), json, instance, { 49 | persisting: false, 50 | })!; 51 | } 52 | 53 | /** 54 | * Adds methods to instances of this factories type 55 | * @param methods The methods to add 56 | * @return 57 | */ 58 | addMethods(methods: { [name: string]: Function }): void { 59 | Object.assign(this.methods, methods); 60 | } 61 | 62 | /** 63 | * Add a method to instances of this factories type 64 | * @param name The method name to add 65 | * @param fn The Method to add 66 | * @return 67 | */ 68 | addMethod(name: string, fn: Function): void { 69 | this.methods[name] = fn; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /spec-ts/typings.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign,@typescript-eslint/no-unused-expressions,@typescript-eslint/no-unused-vars,no-shadow,max-len */ 2 | 3 | import { 4 | db, query, model, metamodel, 5 | } from 'baqend'; 6 | 7 | db.connect('test', true).then(() => { 8 | // db.Test.load('test').then((entity) => { 9 | // 10 | // }); 11 | }); 12 | 13 | db.User.load('id').then((user) => { 14 | user!.newPassword('alodPassword', 'newPassword'); 15 | user!.customUserProp = 'test'; 16 | }); 17 | 18 | db.User.find() 19 | .equal('age', 3) 20 | .between('age', 2, 20) 21 | .equal('name', 'Test QueryPerson') 22 | .in('vals', 3) 23 | .containsAny('vals', [3, 4, 5]) 24 | .singleResult((user) => { 25 | user!.username; 26 | }); 27 | 28 | db.code.loadCode('foo', 'module', true) 29 | .then((code) => { 30 | code.name; 31 | code.call('test', 'world'); 32 | }); 33 | 34 | db.code.loadCode('foo', 'module', false) 35 | .then((code) => { 36 | code.includes('stuff'); 37 | }); 38 | 39 | db.code.loadCode('foo', 'module') 40 | .then((code) => { 41 | code.includes('stuff'); 42 | }); 43 | 44 | const builder:query.Builder = db.Role.find(); 45 | 46 | const q1 = builder.notIn('age', [3, 4, 5]) 47 | .in('places', 2, 3, 4) 48 | .gt('ref', db.User.me!); 49 | 50 | const q2 = builder.equal('name', 'test') 51 | .notEqual('street', 'Mainroad'); 52 | 53 | builder.or(q1, q2) 54 | .singleResult((role) => { 55 | role!.addUser(db.User.me!); 56 | }); 57 | 58 | db.Test.load('test').then((entity) => { 59 | entity!.myProp = 'test'; 60 | const { headers } = entity!.file; 61 | headers.test = 'new header'; 62 | }); 63 | 64 | db.Hallo.find().singleResult().then(() => { 65 | 66 | }); 67 | 68 | db.User.login('test', 'pw').then((user) => { 69 | 70 | }); 71 | 72 | const user = new db.User(); 73 | 74 | db.log.trace('A message'); 75 | db.log.debug('A message'); 76 | db.log.info('A message'); 77 | db.log.warn('A message'); 78 | db.log.error('A message'); 79 | db.log.info('A message with data', { some: 'data' }); 80 | db.log.info('A message with placeholders %d %s', 1, 'string', { some: 'data' }); 81 | 82 | const file = new db.File('test'); 83 | file.upload({ force: true }); 84 | file.loadMetadata().then((file) => { /* ... */ }); 85 | file.loadMetadata({ refresh: true }).then((file) => { /* ... */ }); 86 | 87 | db.modules.get('test', 'test=bla'); 88 | db.modules.get('test', { test: 'bla' }, { responseType: 'json' }); 89 | db.modules.post('test', { test: 'bla' }, { responseType: 'json' }); 90 | 91 | new metamodel.EmbeddableType('Type'); 92 | -------------------------------------------------------------------------------- /lib/metamodel/SingularAttribute.ts: -------------------------------------------------------------------------------- 1 | import { Attribute, PersistentAttributeType } from './Attribute'; 2 | import { PersistenceType, Type } from './Type'; 3 | import { 4 | Class, Json, 5 | } from '../util'; 6 | import { Managed } from '../binding'; 7 | import { ManagedState } from '../intersection'; 8 | 9 | export class SingularAttribute extends Attribute { 10 | public type: Type; 11 | 12 | /** 13 | * The constructor of the element type of this attribute 14 | */ 15 | get typeConstructor(): Class { 16 | return this.type.typeConstructor; 17 | } 18 | 19 | /** 20 | * @inheritDoc 21 | */ 22 | get persistentAttributeType() { 23 | switch (this.type.persistenceType) { 24 | case PersistenceType.BASIC: 25 | return PersistentAttributeType.BASIC; 26 | case PersistenceType.EMBEDDABLE: 27 | return PersistentAttributeType.EMBEDDED; 28 | case PersistenceType.ENTITY: 29 | return PersistentAttributeType.ONE_TO_MANY; 30 | default: 31 | throw new Error('Unknown persistent attribute type.'); 32 | } 33 | } 34 | 35 | /** 36 | * @param name 37 | * @param type 38 | * @param isMetadata true if the attribute is an metadata attribute 39 | */ 40 | constructor(name: string, type: Type, isMetadata?: boolean) { 41 | super(name, isMetadata); 42 | this.type = type; 43 | } 44 | 45 | /** 46 | * @inheritDoc 47 | */ 48 | getJsonValue(state: ManagedState, object: Managed, 49 | options: { excludeMetadata?: boolean; depth?: number | boolean; persisting: boolean }): Json | undefined { 50 | const persistedState: { [key: string]: any } = Attribute.attachState(object, {}); 51 | const value = this.getValue(object); 52 | const changed = persistedState[this.name] !== value; 53 | 54 | if (options.persisting) { 55 | persistedState[this.name] = value; 56 | } 57 | 58 | if (changed) { 59 | state.setDirty(); 60 | } 61 | 62 | return this.type.toJsonValue(state, value, options); 63 | } 64 | 65 | /** 66 | * @inheritDoc 67 | */ 68 | setJsonValue(state: ManagedState, object: Managed, jsonValue: Json, 69 | options: { onlyMetadata?: boolean; persisting: boolean }): void { 70 | const value = this.type.fromJsonValue(state, jsonValue, this.getValue(object), options); 71 | 72 | if (options.persisting) { 73 | const persistedState: { [key: string]: any } = Attribute.attachState(object, {}); 74 | persistedState[this.name] = value; 75 | } 76 | 77 | this.setValue(object, value); 78 | } 79 | 80 | /** 81 | * @inheritDoc 82 | */ 83 | toJSON() { 84 | return { 85 | type: this.type.ref, 86 | ...super.toJSON(), 87 | }; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /web-test-runner.bs.config.mjs: -------------------------------------------------------------------------------- 1 | import { browserstackLauncher } from '@web/test-runner-browserstack'; 2 | import { defaultReporter } from '@web/test-runner'; 3 | import { junitReporter } from '@web/test-runner-junit-reporter'; 4 | 5 | import localConfig from './web-test-runner.config.mjs'; 6 | 7 | // options shared between all browsers 8 | const sharedCapabilities = { 9 | // your username and key for browserstack, you can get this from your browserstack account 10 | // it's recommended to store these as environment variables 11 | 'browserstack.user': process.env.BROWSERSTACK_USERNAME, 12 | 'browserstack.key': process.env.BROWSERSTACK_ACCESS_KEY, 13 | 14 | project: 'JS SDK', 15 | name: 'JS SDK - API Tests', 16 | // if you are running tests in a CI, the build id might be available as an 17 | // environment variable. this is useful for identifying test runs 18 | // this is for example the name for github actions 19 | build: `build ${process.env.CI_JOB_ID || 'unknown'}`, 20 | }; 21 | 22 | export default { 23 | ...localConfig, 24 | name: 'browserstack', 25 | watch: false, 26 | protocol: 'https:', 27 | http2: true, 28 | hostname: 'local.baqend.com', 29 | sslKey: process.env.SSL_KEY ?? 'certs/local-key.pem', 30 | sslCert: process.env.SSL_CERT ?? 'certs/local-cert.pem', 31 | reporters: [ 32 | // use the default reporter only for reporting test progress 33 | defaultReporter({ reportTestResults: false, reportTestProgress: true }), 34 | // use another reporter to report test results 35 | junitReporter({ 36 | outputPath: './browserstack-result.xml', 37 | reportLogs: true, // default `false` 38 | }), 39 | ], 40 | concurrentBrowsers: 1, 41 | // amount of test files to execute concurrently in a browser. the default value is based 42 | // on amount of available CPUs locally which is irrelevant when testing remotely 43 | concurrency: 1, 44 | localOptions: { 45 | forceLocal: true, 46 | }, 47 | browsers: [ 48 | // create a browser launcher per browser you want to test 49 | // you can get the browser capabilities from the browserstack website 50 | browserstackLauncher({ 51 | capabilities: { 52 | ...sharedCapabilities, 53 | browser: 'Chrome', 54 | os: 'Windows', 55 | }, 56 | }), 57 | 58 | browserstackLauncher({ 59 | capabilities: { 60 | ...sharedCapabilities, 61 | browser: 'Safari', 62 | browser_version: '16', 63 | os: 'OS X', 64 | }, 65 | }), 66 | 67 | browserstackLauncher({ 68 | capabilities: { 69 | ...sharedCapabilities, 70 | browser: 'Firefox', 71 | os: 'Windows', 72 | }, 73 | }), 74 | ].filter(b => !process.env.BROWSER || b.name.startsWith(process.env.BROWSER)), 75 | }; -------------------------------------------------------------------------------- /lib/metamodel/DbIndex.ts: -------------------------------------------------------------------------------- 1 | import { JsonMap } from '../util'; 2 | 3 | type IndexSpec = { [name: string]: string }[]; 4 | 5 | /** 6 | * Creates a new index instance which is needed to create an 7 | * database index. 8 | */ 9 | export class DbIndex { 10 | public static readonly ASC = 'asc'; 11 | 12 | public static readonly DESC = 'desc'; 13 | 14 | public static readonly GEO: 'geo'; 15 | 16 | /** 17 | * An array of mappings from field to index type which are parts of this index/compound index 18 | */ 19 | public keys: IndexSpec; 20 | 21 | public unique: boolean; 22 | 23 | /** 24 | * Returns DbIndex Object created from the given JSON 25 | * @param json 26 | * @return 27 | */ 28 | public static fromJSON(json: JsonMap): DbIndex { 29 | return new DbIndex(json.keys as IndexSpec, json.unique as boolean); 30 | } 31 | 32 | /** 33 | * @param keys The name of the field which will be used 34 | * for the index, 35 | * an object of an field and index type combination or 36 | * an array of objects to create an compound index 37 | * @param unique Indicates if the index will be unique 38 | */ 39 | constructor(keys: string | { [name: string]: string } | IndexSpec, unique?: boolean) { 40 | if (typeof keys === 'string') { 41 | const key: { [p: string]: string } = {}; 42 | key[keys] = DbIndex.ASC; 43 | this.keys = [key]; 44 | } else if (Array.isArray(keys)) { 45 | this.keys = keys; 46 | } else if (keys) { 47 | this.keys = [keys]; 48 | } else { 49 | throw new Error('The keys parameter must be an String, Object or Array.'); 50 | } 51 | 52 | this.unique = unique === true; 53 | } 54 | 55 | /** 56 | * Indicates if this index is for the given field or includes it in a compound index 57 | * @param name The name of the field to check for 58 | * @return true if the index contains this field 59 | */ 60 | hasKey(name: string): boolean { 61 | for (let i = 0; i < this.keys.length; i += 1) { 62 | if (this.keys[i][name]) { 63 | return true; 64 | } 65 | } 66 | return false; 67 | } 68 | 69 | /** 70 | * Indicates if this index is a compound index of multiple attributes 71 | * @type boolean 72 | * @readonly 73 | */ 74 | get isCompound() { 75 | return this.keys.length > 1; 76 | } 77 | 78 | /** 79 | * Indicates if this index is an unique index 80 | * @type boolean 81 | * @readonly 82 | */ 83 | get isUnique() { 84 | return this.unique; 85 | } 86 | 87 | /** 88 | * Returns a JSON representation of the Index object 89 | * 90 | * @return A Json of this Index object 91 | */ 92 | toJSON(): JsonMap { 93 | return { 94 | unique: this.unique, 95 | keys: this.keys, 96 | }; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/binding/Managed.ts: -------------------------------------------------------------------------------- 1 | import { enumerable } from '../util/enumerable'; 2 | import { Class, Json } from '../util'; 3 | import { Enhancer } from './Enhancer'; 4 | import { Metadata } from '../intersection'; 5 | import type { ManagedState } from '../intersection/Metadata'; 6 | import type { EntityType } from '../metamodel'; 7 | 8 | export interface Managed { 9 | /** 10 | * The managed properties of this object 11 | */ 12 | [property: string]: any; 13 | 14 | /** 15 | * Contains the metadata of this managed object 16 | */ 17 | _metadata: ManagedState; 18 | } 19 | export class Managed { 20 | /** 21 | * Initialize the given instance 22 | * @param instance The managed instance to initialize 23 | * @param properties The optional properties to set on the instance 24 | */ 25 | static init(instance: Managed, properties?: { [property: string]: any }): void { 26 | const type = Enhancer.getBaqendType(instance.constructor)!; 27 | if (type.isEntity) { 28 | Object.defineProperty(instance, '_metadata', { 29 | value: Metadata.create(type as EntityType), 30 | configurable: true, 31 | }); 32 | } 33 | 34 | if (properties) { 35 | Object.assign(instance, properties); 36 | } 37 | } 38 | 39 | /** 40 | * Creates a subclass of this class 41 | * @param {Class<*>} childClass 42 | * @return {Class<*>} The extended child class 43 | */ 44 | static extend(childClass: Class | Function): Class | Function { 45 | // eslint-disable-next-line no-param-reassign 46 | childClass.prototype = Object.create(this.prototype, { 47 | constructor: { 48 | value: childClass, 49 | configurable: true, 50 | writable: true, 51 | }, 52 | }); 53 | // eslint-disable-next-line no-param-reassign 54 | (childClass as any).extend = Managed.extend; 55 | return childClass; 56 | } 57 | 58 | /** 59 | * The default constructor, copy all given properties to this object 60 | * @param properties - The optional properties to copy 61 | */ 62 | constructor(properties?: { [property: string]: any }) { 63 | Managed.init(this, properties); 64 | } 65 | 66 | /** 67 | * Returns this object identifier or the baqend type of this object 68 | * @return the object id or type whatever is available 69 | */ 70 | @enumerable(false) 71 | toString(): string { 72 | const type = Enhancer.getBaqendType(this.constructor); 73 | return type!.ref; 74 | } 75 | 76 | /** 77 | * Converts the managed object to an JSON-Object. 78 | * @return JSON-Object 79 | * @method 80 | */ 81 | @enumerable(false) 82 | toJSON(): Json { 83 | const type = Enhancer.getBaqendType(this.constructor)!; 84 | return type.toJsonValue(Metadata.create(type), this, { persisting: false }); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/util/Lockable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This base class provides an lock interface to execute exclusive operations 3 | */ 4 | export class Lockable { 5 | private isLocked: boolean; 6 | 7 | private readyPromise: Promise; 8 | 9 | constructor() { 10 | /** 11 | * Indicates if there is currently an onging exclusive operation 12 | * @type boolean 13 | * @private 14 | */ 15 | this.isLocked = false; 16 | 17 | /** 18 | * A promise which represents the state of the least exclusive operation 19 | * @type Promise 20 | * @private 21 | */ 22 | this.readyPromise = Promise.resolve(this); 23 | } 24 | 25 | /** 26 | * Indicates if there is currently no exclusive operation executed 27 | * true If no exclusive lock is hold 28 | */ 29 | get isReady(): boolean { 30 | return !this.isLocked; 31 | } 32 | 33 | /** 34 | * Waits on the previously requested operation and calls the doneCallback if the operation is fulfilled 35 | * @param doneCallback The callback which will be invoked when the previously 36 | * operations on this object is completed. 37 | * @param failCallback When the lock can't be released caused by a none 38 | * recoverable error 39 | * @return A promise which completes successfully, when the previously requested 40 | * operation completes 41 | */ 42 | ready(doneCallback?: (this: this) => any, failCallback?: (error: Error) => any): Promise { 43 | return this.readyPromise.then(doneCallback, failCallback); 44 | } 45 | 46 | /** 47 | * Try to aquire an exclusive lock and executes the given callback. 48 | * @param callback The exclusive operation to execute 49 | * @param [critical=false] Indicates if the operation is critical. If the operation is critical and the 50 | * operation fails, then the lock will not be released 51 | * @return A promise 52 | * @throws If the lock can't be aquired 53 | * @protected 54 | */ 55 | withLock(callback: () => Promise, critical = false): Promise { 56 | if (this.isLocked) { 57 | throw new Error('Current operation has not been finished.'); 58 | } 59 | 60 | try { 61 | this.isLocked = true; 62 | const result = callback().then((res) => { 63 | this.isLocked = false; 64 | return res; 65 | }, (e) => { 66 | if (!critical) { 67 | this.isLocked = false; 68 | } 69 | throw e; 70 | }); 71 | 72 | this.readyPromise = result.then(() => this, (e) => { 73 | if (!critical) { 74 | return this; 75 | } 76 | throw e; 77 | }); 78 | 79 | return result; 80 | } catch (e) { 81 | if (critical) { 82 | this.readyPromise = Promise.reject(e); 83 | } else { 84 | this.isLocked = false; 85 | } 86 | throw e; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const TerserPlugin = require('terser-webpack-plugin'); 4 | const EsmWebpackPlugin = require('@purtuga/esm-webpack-plugin'); 5 | const fs = require('fs'); 6 | const pkg = require('./package.json'); 7 | 8 | const copyright = fs.readFileSync('LICENSE.md', { encoding: 'utf-8' }).split(/[\r\n]/)[0]; 9 | 10 | const date = new Date().toUTCString(); 11 | const longBanner = `/*! 12 | * ${pkg.description} ${pkg.version} 13 | * ${pkg.homepage} 14 | * 15 | * ${copyright} 16 | * 17 | * Includes: 18 | * uuid - https://github.com/uuidjs/uuid 19 | * Copyright (c) 2010-2020 Robert Kieffer and other contributors 20 | * 21 | * Released under the MIT license 22 | * 23 | * Date: ${date} 24 | */ 25 | `; 26 | 27 | const tsOptions = { 28 | es5: { 29 | target: 'es5', 30 | module: 'commonjs', 31 | importHelpers: true, 32 | }, 33 | es2015: { 34 | target: 'es6', 35 | module: 'es2015', 36 | }, 37 | }; 38 | 39 | function bundleLib(target) { 40 | return { 41 | name: target, 42 | mode: 'production', 43 | entry: { 44 | [`baqend.${target}`]: './lib/index.ts', 45 | [`baqend.${target}.min`]: './lib/index.ts', 46 | }, 47 | output: { 48 | path: path.resolve(__dirname, 'dist/'), 49 | filename: '[name].js', 50 | libraryTarget: target === 'es2015' ? 'var' : 'umd', 51 | library: 'Baqend', 52 | umdNamedDefine: target !== 'es2015', 53 | }, 54 | externals: { 55 | validator: 'validator', 56 | }, 57 | resolve: { 58 | extensions: ['.ts'], 59 | aliasFields: ['browser'], 60 | }, 61 | devtool: 'source-map', 62 | node: false, 63 | optimization: { 64 | minimize: true, 65 | minimizer: [ 66 | new TerserPlugin({ 67 | include: /\.min\.js$/, 68 | extractComments: { 69 | banner: (licenseFile) => `${pkg.name} ${pkg.version} | ${copyright} | License information ${licenseFile}`, 70 | }, 71 | }), 72 | ], 73 | concatenateModules: target === 'es2015', 74 | }, 75 | module: { 76 | rules: [ 77 | { 78 | test: /\.ts$/, 79 | exclude: [/node_modules/], 80 | loader: 'ts-loader', 81 | options: { 82 | onlyCompileBundledFiles: true, 83 | configFile: 'tsconfig.lib.json', 84 | compilerOptions: tsOptions[target], 85 | }, 86 | }, 87 | ], 88 | }, 89 | stats: { 90 | optimizationBailout: true, 91 | }, 92 | plugins: [ 93 | ...(target === 'es2015' ? [new EsmWebpackPlugin()] : []), 94 | new webpack.BannerPlugin({ 95 | banner: longBanner, 96 | raw: true, 97 | entryOnly: true, 98 | }), 99 | ], 100 | }; 101 | } 102 | 103 | module.exports = [ 104 | bundleLib('es5'), 105 | bundleLib('es2015'), 106 | ]; 107 | -------------------------------------------------------------------------------- /lib/binding/Factory.ts: -------------------------------------------------------------------------------- 1 | import { Class } from '../util/Class'; 2 | 3 | /** 4 | * This factory creates instances of type T, by invoking the {@link #new()} method 5 | * or by instantiating this factory directly 6 | */ 7 | export interface InstanceFactory { 8 | /** 9 | * Creates a new instance of the factory type 10 | * @param args Constructor arguments used for instantiation 11 | * @return A new created instance of * 12 | * @instance 13 | */ 14 | new(...args: any[]): T 15 | } 16 | 17 | export class Factory { 18 | private static extend>(target: T, proto: P): T & P { 19 | if (proto !== Factory.prototype) { 20 | this.extend(target, Object.getPrototypeOf(proto)); 21 | } 22 | 23 | const properties = Object.getOwnPropertyNames(proto); 24 | for (let j = 0, len = properties.length; j < len; j += 1) { 25 | const prop = properties[j]; 26 | Object.defineProperty(target, prop, Object.getOwnPropertyDescriptor(proto, prop)!); 27 | } 28 | 29 | return target as T & P; 30 | } 31 | 32 | /** 33 | * Creates a new Factory for the given type 34 | * @param type - the type constructor of T 35 | * @return A new object factory to created instances of T 36 | */ 37 | protected static createFactory, T>(this: Class, type: Class): F & InstanceFactory { 38 | // We want te explicitly name the created factory and give the constructor a properly argument name 39 | const factory = Factory.extend((function FactoryConstructor(...args: any[]) { 40 | return factory.newInstance(args); 41 | }) as any as F, this.prototype); 42 | 43 | // lets instanceof work properly 44 | factory.prototype = type.prototype; 45 | factory.type = type; 46 | 47 | return factory; 48 | } 49 | 50 | public type: Class = null as any; 51 | 52 | public prototype: T = null as any; 53 | 54 | /** 55 | * Creates a new instance of the factory type 56 | * @param args Constructor arguments used for instantiation 57 | * @return A new created instance of * 58 | * @instance 59 | */ 60 | new(...args: any[]): T { 61 | return this.newInstance!(args); 62 | } 63 | 64 | /** 65 | * Creates a new instance of the factory type 66 | * @param args Constructor arguments used for instantiation 67 | * @return A new created instance of * 68 | * @instance 69 | */ 70 | newInstance(args?: any[] | IArguments): T { 71 | if (!args || args.length === 0) { 72 | // eslint-disable-next-line new-cap 73 | return new this.type!(); 74 | } 75 | 76 | // es6 constructors can't be called, therefore bind all arguments and invoke the constructor 77 | // then with the bounded parameters 78 | // The first argument is shift out by invocation with `new`. 79 | const a: [any] = [null]; 80 | Array.prototype.push.apply(a, args as any[]); 81 | const boundConstructor = (Function.prototype.bind.apply(this.type!, a)); 82 | // eslint-disable-next-line new-cap 83 | return new boundConstructor(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /connect.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('message', send, false); 2 | var basePath = location.pathname.substring(0, location.pathname.lastIndexOf('/')); 3 | function send(event) { 4 | if (!event.data) return; 5 | 6 | var msg = JSON.parse(event.data); 7 | 8 | if (!msg.mid) return; 9 | 10 | msg.origin = event.origin; 11 | msg.source = event.source; 12 | 13 | if (msg.method === 'OAUTH') { 14 | return handleOAuth(msg); 15 | } 16 | 17 | var node = msg.method === 'GET' && document.getElementById(msg.path); 18 | if(!node) { 19 | var xhr = new XMLHttpRequest(); 20 | 21 | xhr.onreadystatechange = function() { 22 | if (xhr.readyState === 4) { 23 | var headers = {}; 24 | msg.responseHeaders.forEach(function(name) { 25 | headers[name] = xhr.getResponseHeader(name); 26 | }); 27 | receive(msg, xhr.status, headers, xhr.responseText); 28 | } 29 | }; 30 | 31 | xhr.open(msg.method, basePath + msg.path, true); 32 | for (var name in msg.headers) 33 | xhr.setRequestHeader(name, msg.headers[name]); 34 | 35 | xhr.send(msg.entity); 36 | } else { 37 | applyCacheRule(node); 38 | receive(msg, node.text? 200: 404, getHeaders(node), node.text); 39 | } 40 | } 41 | 42 | function receive(message, status, headers, entity) { 43 | var response = { 44 | mid: message.mid, 45 | status: status, 46 | headers: headers, 47 | entity: entity 48 | }; 49 | 50 | if (message.origin === 'null' || message.origin === 'file:') 51 | message.origin = '*'; 52 | 53 | message.source.postMessage(JSON.stringify(response), message.origin); 54 | } 55 | 56 | function applyCacheRule(node) { 57 | var cacheControl = node.getAttribute('data-cache-control'); 58 | if(~cacheControl.indexOf('no-cache')) { 59 | node.parentNode.removeChild(node); 60 | } 61 | } 62 | 63 | function getHeaders(node) { 64 | var headers = {'Content-Type': 'application/json'}; 65 | var token = node.getAttribute('data-token'); 66 | if (token) 67 | headers['baqend-authorization-token'] = token; 68 | return headers; 69 | } 70 | 71 | var oAuthHandle, oAuthInterval; 72 | function handleOAuth(msg) { 73 | if (oAuthHandle) 74 | oAuthHandle(409, {}, '{"message": "A new OAuth request was sent."}'); 75 | 76 | localStorage.removeItem('oauth-response'); 77 | 78 | var handler = function(event) { 79 | if (event.key === 'oauth-response') { 80 | var response = JSON.parse(event.newValue); 81 | oAuthHandle(response.status, response.headers, response.entity); 82 | } 83 | }; 84 | 85 | oAuthHandle = function(status, headers, entity) { 86 | receive(msg, status, headers, entity); 87 | localStorage.removeItem('oauth-response'); 88 | removeEventListener("storage", handler, false); 89 | clearTimeout(oAuthInterval); 90 | }; 91 | 92 | addEventListener("storage", handler, false); 93 | oAuthInterval = setInterval(function() { 94 | var item = localStorage.getItem('oauth-response'); 95 | if (item) { 96 | handler({key: 'oauth-response', newValue: item}); 97 | } 98 | }, 500); 99 | } 100 | -------------------------------------------------------------------------------- /lib/binding/EntityFactory.ts: -------------------------------------------------------------------------------- 1 | import { ManagedFactory } from './ManagedFactory'; 2 | import type { Entity } from './Entity'; 3 | import { Json, JsonMap } from '../util'; 4 | import { Builder } from '../query'; 5 | import { EntityPartialUpdateBuilder } from '../partialupdate'; 6 | import { Metadata } from '../intersection'; 7 | 8 | export class EntityFactory extends ManagedFactory { 9 | /** 10 | * Creates a new instance of the factory type 11 | * 12 | * @param args Constructor arguments used for instantiation, the constructor will not be called 13 | * when no arguments are passed 14 | * @return A new created instance of T 15 | */ 16 | newInstance(args?: any[] | IArguments) { 17 | const instance = super.newInstance(args); 18 | Metadata.get(instance).db = this.db; 19 | return instance; 20 | } 21 | 22 | /** 23 | * Loads the instance for the given id, or null if the id does not exists. 24 | * @param id The id to query 25 | * @param [options] The load options 26 | * @param [options.depth=0] The object depth which will be loaded. Depth 0 loads only this object, 27 | * true loads the objects by reachability. 28 | * @param [options.refresh=false] Indicates whether the object should be revalidated (cache bypass). 29 | * @param [options.local=false] Indicates whether the local copy (from the entity manager) 30 | * of an object should be returned if it exists. This value might be stale. 31 | * @param doneCallback Called when the operation succeed. 32 | * @param failCallback Called when the operation failed. 33 | * @return A Promise that will be fulfilled when the asynchronous operation completes. 34 | */ 35 | load(id: string, options?: { depth?: number | boolean, refresh?: boolean, local?: boolean, }, doneCallback?: any, 36 | failCallback?: any): Promise { 37 | if (typeof options === 'function') { 38 | return this.load(id, {}, options, doneCallback); 39 | } 40 | 41 | return this.db.load(this.managedType.typeConstructor, id, options).then(doneCallback, failCallback); 42 | } 43 | 44 | /** 45 | * Gets an unloaded reference for the given id. 46 | * @param id The id of an object to get a reference for. 47 | * @return An unloaded reference to the object with the given id. 48 | */ 49 | ref(id: string): T { 50 | return this.db.getReference(this.managedType.ref, id); 51 | } 52 | 53 | /** 54 | * Creates a new instance and sets the DatabaseObject to the given json 55 | * @param json 56 | * @return instance 57 | */ 58 | fromJSON(json: Json): T { 59 | const obj: T = this.db.getReference(this.managedType.ref, (json as JsonMap).id as string); 60 | return this.managedType.fromJsonValue(Metadata.get(obj), json, obj, { persisting: false })!; 61 | } 62 | 63 | /** 64 | * Creates a new query for this class 65 | * @return The query builder 66 | */ 67 | find(): Builder { 68 | return this.db.createQueryBuilder(this.managedType.typeConstructor); 69 | } 70 | 71 | /** 72 | * Creates a new partial update for this class 73 | * @param id The id to partial update 74 | * @param [partialUpdate] An initial partial update to execute 75 | * @return A partial update builder for the given entity id 76 | */ 77 | partialUpdate(id: string, partialUpdate?: Json): EntityPartialUpdateBuilder { 78 | return this.ref(id).partialUpdate(partialUpdate); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/metamodel/ListAttribute.ts: -------------------------------------------------------------------------------- 1 | import { CollectionType, PluralAttribute } from './PluralAttribute'; 2 | import { Attribute } from './Attribute'; 3 | import { JsonArray } from '../util'; 4 | import { Type } from './Type'; 5 | import { Managed } from '../binding'; 6 | import { ManagedState } from '../intersection/Metadata'; 7 | 8 | export class ListAttribute extends PluralAttribute, E> { 9 | /** 10 | * Get the type id for this list type 11 | */ 12 | static get ref(): string { 13 | return '/db/collection.List'; 14 | } 15 | 16 | /** 17 | * @inheritDoc 18 | */ 19 | get collectionType() { 20 | return CollectionType.LIST; 21 | } 22 | 23 | /** 24 | * @param name 25 | * @param elementType 26 | */ 27 | constructor(name: string, elementType: Type) { 28 | super(name, Array, elementType); 29 | } 30 | 31 | /** 32 | * @inheritDoc 33 | */ 34 | getJsonValue(state: ManagedState, object: Managed, 35 | options: { excludeMetadata?: boolean; depth?: number | boolean; persisting: boolean }): JsonArray | null { 36 | const value = this.getValue(object); 37 | 38 | if (!(value instanceof this.typeConstructor)) { 39 | return null; 40 | } 41 | 42 | const len = value.length; 43 | const persisting: (E | null)[] = new Array(len); 44 | const attachedState: any[] | undefined = Attribute.attachState(value); 45 | const persistedState = attachedState || []; 46 | 47 | let changed = !attachedState || attachedState.length !== len; 48 | 49 | const json: JsonArray = new Array(len); 50 | for (let i = 0; i < len; i += 1) { 51 | const el = value[i]; 52 | json[i] = this.elementType.toJsonValue(state, el, options); 53 | persisting[i] = el; 54 | 55 | changed = changed || persistedState[i] !== el; 56 | } 57 | 58 | if (options.persisting) { 59 | Attribute.attachState(value, persisting, true); 60 | } 61 | 62 | if (changed) { 63 | state.setDirty(); 64 | } 65 | 66 | return json; 67 | } 68 | 69 | /** 70 | * @inheritDoc 71 | */ 72 | setJsonValue(state: ManagedState, object: Managed, json: JsonArray, 73 | options: { onlyMetadata?: boolean; persisting: boolean }): void { 74 | let value: (E | null)[] | null = null; 75 | 76 | if (json) { 77 | value = this.getValue(object); 78 | 79 | const len = json.length; 80 | if (!(value instanceof this.typeConstructor)) { 81 | value = new this.typeConstructor(len); // eslint-disable-line new-cap 82 | } 83 | 84 | const persisting = new Array(len); 85 | const persistedState: any[] = Attribute.attachState(value) || []; 86 | 87 | // clear additional items 88 | if (len < value.length) { 89 | value.splice(len, value.length - len); 90 | } 91 | 92 | for (let i = 0; i < len; i += 1) { 93 | const el = this.elementType.fromJsonValue(state, json[i], persistedState[i], options); 94 | value[i] = el; 95 | persisting[i] = el; 96 | } 97 | 98 | if (options.persisting) { 99 | Attribute.attachState(value, persisting, true); 100 | } 101 | } 102 | 103 | this.setValue(object, value); 104 | } 105 | 106 | /** 107 | * @inheritDoc 108 | */ 109 | toJSON() { 110 | return { 111 | type: `${ListAttribute.ref}[${this.elementType.ref}]`, 112 | ...super.toJSON(), 113 | }; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /lib/binding/DeviceFactory.ts: -------------------------------------------------------------------------------- 1 | import * as message from '../message'; 2 | import { PushMessage } from '../intersection'; 3 | import type * as model from '../model'; 4 | import { EntityFactory } from './EntityFactory'; 5 | 6 | export class DeviceFactory extends EntityFactory { 7 | /** 8 | * Push message will be used to send a push notification to a set of devices 9 | */ 10 | public get PushMessage() { 11 | return PushMessage; 12 | } 13 | 14 | /** 15 | * The current registered device, or null if the device is not registered 16 | * @type model.Device 17 | */ 18 | get me() { 19 | return this.db.deviceMe; 20 | } 21 | 22 | /** 23 | * Returns true if the devices is already registered, otherwise false. 24 | * @type boolean 25 | */ 26 | get isRegistered() { 27 | return this.db.isDeviceRegistered; 28 | } 29 | 30 | /** 31 | * Loads the Public VAPID Key which can be used to subscribe a Browser for Web Push notifications 32 | * @return The public VAPID Web Push subscription key 33 | */ 34 | loadWebPushKey(): Promise { 35 | const msg = new message.VAPIDPublicKey(); 36 | msg.responseType('arraybuffer'); 37 | return this.db.send(msg).then((response) => response.entity); 38 | } 39 | 40 | /** 41 | * Register a new device with the given device token and OS. 42 | * @param os The OS of the device (IOS/Android) 43 | * @param tokenOrSubscription The FCM device token, APNS device token or WebPush subscription 44 | * @param doneCallback Called when the operation succeed. 45 | * @param failCallback Called when the operation failed. 46 | * @return The registered device 47 | */ 48 | register(os: string, tokenOrSubscription: string | PushSubscription, doneCallback?: any, failCallback?: any): 49 | Promise; 50 | 51 | /** 52 | * Register a new device with the given device token and OS. 53 | * @param os The OS of the device (IOS/Android) 54 | * @param tokenOrSubscription The FCM device token, APNS device token or WebPush 55 | * subscription 56 | * @param device An optional device entity to set custom field values 57 | * @param doneCallback Called when the operation succeed. 58 | * @param failCallback Called when the operation failed. 59 | * @return The registered device 60 | */ 61 | register(os: string, tokenOrSubscription: string | PushSubscription, device: model.Device | null, doneCallback?: any, 62 | failCallback?: any): Promise; 63 | 64 | register(os: string, tokenOrSubscription: string | PushSubscription, device: model.Device | Function | null, 65 | doneCallback?: any, failCallback?: any): Promise { 66 | if (device instanceof Function) { 67 | return this.register(os, tokenOrSubscription, null, device, doneCallback); 68 | } 69 | 70 | const subscription = typeof tokenOrSubscription === 'string' ? { token: tokenOrSubscription } : tokenOrSubscription; 71 | 72 | return this.db.registerDevice(os, subscription, device).then(doneCallback, failCallback); 73 | } 74 | 75 | /** 76 | * Uses the info from the given {@link PushMessage} message to send an push notification. 77 | * @param pushMessage to send an push notification. 78 | * @param doneCallback Called when the operation succeed. 79 | * @param failCallback Called when the operation failed. 80 | * @return 81 | */ 82 | push(pushMessage: PushMessage, doneCallback?: any, failCallback?: any): Promise { 83 | return this.db.pushDevice(pushMessage).then(doneCallback, failCallback); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/intersection/Modules.ts: -------------------------------------------------------------------------------- 1 | import * as message from '../message'; 2 | import type { EntityManager } from '../EntityManager'; 3 | import { 4 | RequestBody, RequestBodyType, ResponseBodyType, Message, 5 | } from '../connector'; 6 | 7 | /** 8 | * An executor of Modules running on Baqend. 9 | */ 10 | export class Modules { 11 | entityManager: EntityManager; 12 | 13 | /** 14 | * @param entityManager 15 | */ 16 | constructor(entityManager: EntityManager) { 17 | this.entityManager = entityManager; 18 | } 19 | 20 | /** 21 | * Calls the module, which is identified by the given bucket 22 | * 23 | * The optional query parameter will be attached as GET-parameters. 24 | * 25 | * @param bucket Name of the module 26 | * @param query GET-Parameter as key-value-pairs or query string 27 | * @param options Additional request options 28 | * @param options.responseType The type used to provide the response data, defaults to text oder json 29 | * depends on the received data, can be one of arraybuffer, blob, json, text, base64, data-url 30 | * @param doneCallback 31 | * @param failCallback 32 | * @return 33 | */ 34 | get(bucket: string, query?: { [param: string]: string } | string | Function, 35 | options?: { responseType?: ResponseBodyType } | Function, doneCallback?: any, failCallback?: any): Promise { 36 | if (query instanceof Function) { 37 | return this.get(bucket, {}, query, options, doneCallback); 38 | } 39 | 40 | if (options instanceof Function) { 41 | return this.get(bucket, query, {}, options, doneCallback); 42 | } 43 | 44 | const opt = options || {}; 45 | 46 | const msg = new message.GetBaqendModule(bucket) 47 | .addQueryString(query || '') 48 | .responseType(opt.responseType || null); 49 | 50 | return this.send(msg, doneCallback, failCallback); 51 | } 52 | 53 | /** 54 | * Calls the module, which is identified by the given bucket 55 | * 56 | * @param bucket Name of the module 57 | * @param [body] The POST-body data to send 58 | * @param options Additional request options 59 | * @param options.requestType A optional type hint used to correctly interpret the provided data, can be one 60 | * of arraybuffer, blob, json, text, base64, data-url, form 61 | * @param options.mimeType The mimType of the body. Defaults to the mimeType of the provided data if 62 | * it is a file object, blob or data-url 63 | * @param options.responseType The type used to provide the response data, defaults to text oder json 64 | * depends on the received data, can be one of arraybuffer, blob, json, text, base64, data-url 65 | * @param doneCallback 66 | * @param failCallback 67 | * @return 68 | */ 69 | post(bucket: string, body: RequestBody, options?: { requestType?: RequestBodyType, mimeType?: string, 70 | responseType?: ResponseBodyType }, doneCallback?: any, failCallback?: any): Promise { 71 | if (typeof options === 'function') { 72 | return this.post(bucket, body, {}, options, doneCallback); 73 | } 74 | 75 | const opt = options || {}; 76 | 77 | const msg = new message.PostBaqendModule(bucket) 78 | .entity(body, opt.requestType) 79 | .mimeType(opt.mimeType || null) 80 | .responseType(opt.responseType || null); 81 | 82 | return this.send(msg, doneCallback, failCallback); 83 | } 84 | 85 | send(msg: Message, doneCallback?: any, failCallback?: any) { 86 | return this.entityManager.send(msg) 87 | .then((response) => response.entity) 88 | .then(doneCallback, failCallback); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/query/Builder.ts: -------------------------------------------------------------------------------- 1 | import { Filter } from './Filter'; 2 | import { Condition } from './Condition'; 3 | import { Operator } from './Operator'; 4 | import { 5 | FailCallback, 6 | Query, 7 | ResultOptions, ResultListCallback, SingleResultCallback, CountCallback, flatArgs, 8 | } from './Query'; 9 | import type { Entity } from '../binding'; 10 | import { Node } from './Node'; 11 | 12 | /** 13 | * The Query Builder allows creating filtered and combined queries 14 | */ 15 | export interface Builder extends Query, Condition {} // mixin the condition implementation 16 | export class Builder extends Query { 17 | /** 18 | * Joins the conditions by an logical AND 19 | * @param args The query nodes to join 20 | * @return Returns a new query which joins the given queries by a logical AND 21 | */ 22 | and(...args: Array | Query[]>): Operator { 23 | return this.addOperator('$and', flatArgs(args)); 24 | } 25 | 26 | /** 27 | * Joins the conditions by an logical OR 28 | * @param args The query nodes to join 29 | * @return Returns a new query which joins the given queries by a logical OR 30 | */ 31 | or(...args: Array | Query[]>): Operator { 32 | return this.addOperator('$or', flatArgs(args)); 33 | } 34 | 35 | /** 36 | * Joins the conditions by an logical NOR 37 | * @param args The query nodes to join 38 | * @return Returns a new query which joins the given queries by a logical NOR 39 | */ 40 | nor(...args: Array | Query[]>): Operator { 41 | return this.addOperator('$nor', flatArgs(args)); 42 | } 43 | 44 | /** 45 | * @inheritDoc 46 | */ 47 | resultList(options?: ResultOptions | ResultListCallback, 48 | doneCallback?: ResultListCallback | FailCallback, failCallback?: FailCallback) { 49 | return this.where({}).resultList(options, doneCallback, failCallback); 50 | } 51 | 52 | /** 53 | * @inheritDoc 54 | */ 55 | singleResult(options?: ResultOptions | SingleResultCallback, 56 | doneCallback?: SingleResultCallback | FailCallback, failCallback?: FailCallback) { 57 | return this.where({}).singleResult(options, doneCallback, failCallback); 58 | } 59 | 60 | /** 61 | * @inheritDoc 62 | */ 63 | count(doneCallback?: CountCallback, failCallback?: FailCallback) { 64 | return this.where({}).count(doneCallback, failCallback); 65 | } 66 | 67 | addOperator(operator: string, args: Node[]) { 68 | if (args.length < 2) { 69 | throw new Error(`Only two or more queries can be joined with an ${operator} operator.`); 70 | } 71 | 72 | args.forEach((arg, index) => { 73 | if (!(arg instanceof Node)) { 74 | throw new Error(`Argument at index ${index} is not a query.`); 75 | } 76 | }); 77 | 78 | return new Operator(this.entityManager, this.resultClass, operator, args); 79 | } 80 | 81 | addOrder(fieldOrSort: string | { [field: string]: 1 | -1 }, order?: 1 | -1) { 82 | return new Filter(this.entityManager, this.resultClass).addOrder(fieldOrSort, order); 83 | } 84 | 85 | addFilter(field: string | null, filter: string | null, value: any): Filter { 86 | return new Filter(this.entityManager, this.resultClass).addFilter(field, filter, value); 87 | } 88 | 89 | addOffset(offset: number) { 90 | return new Filter(this.entityManager, this.resultClass).addOffset(offset); 91 | } 92 | 93 | addLimit(limit: number) { 94 | return new Filter(this.entityManager, this.resultClass).addLimit(limit); 95 | } 96 | } 97 | 98 | Object.assign(Builder.prototype, Condition); 99 | -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -o pipefail 5 | 6 | # Redirect stderr to stdout for all commands in this script 7 | exec 2>&1 8 | 9 | # Read command line flags 10 | while [[ $# -gt 0 ]]; do 11 | key="$1" 12 | case $key in 13 | --action) 14 | ACTION="$2" 15 | shift # past argument 16 | shift # past value 17 | ;; 18 | --version) 19 | VERSION="$2" 20 | shift # past argument 21 | shift # past value 22 | ;; 23 | --channel) 24 | CHANNEL="$2" 25 | shift # past argument 26 | shift # past value 27 | ;; 28 | --assets) 29 | ASSETS="$2" 30 | shift # past argument 31 | shift # past value 32 | ;; 33 | --project) 34 | PROJECT="$2" 35 | shift # past argument 36 | shift # past value 37 | ;; 38 | --base-path) 39 | BASE_PATH="$2" 40 | shift # past argument 41 | shift # past value 42 | ;; 43 | *) # Unknown option 44 | echo "Unknown option: $1" 45 | exit 1 46 | ;; 47 | esac 48 | done 49 | 50 | # Functions for different actions 51 | publish() { 52 | echo "Publishing artefacts to $BASE_PATH $PROJECT $VERSION $CHANNEL $ASSETS" 53 | 54 | # Target directory 55 | target_dir=".s3" 56 | 57 | # Ensure target directory exists 58 | mkdir -p "$target_dir" 59 | 60 | # Split variable by comma and iterate 61 | IFS=',' read -ra ADDR <<< "$ASSETS" 62 | for path in "${ADDR[@]}"; do 63 | # Check if the path exists before copying 64 | if [[ -e $path ]]; then 65 | cp -r "$path"/* "$target_dir/" 66 | else 67 | echo "Warning: Path $path does not exist!" 68 | fi 69 | done 70 | 71 | aws s3 sync $target_dir $BASE_PATH/$PROJECT/$CHANNEL/ --profile s3-publish --delete --metadata "surrogate-key=$PROJECT-$CHANNEL,version=$VERSION" --cache-control "max-age=0" 72 | aws s3 sync $target_dir $BASE_PATH/$PROJECT/$VERSION/ --profile s3-publish --metadata "surrogate-key=$PROJECT,version=$VERSION" --cache-control "public, max-age=31536000" 73 | 74 | curl --fail-with-body -X POST -H "Fastly-Key: $FASTLY_KEY" "https://api.fastly.com/service/$FASTLY_SERVICE/purge/$PROJECT-$CHANNEL" 75 | } 76 | 77 | verify() { 78 | variables=("FASTLY_KEY" "FASTLY_SERVICE" "AWS_CONFIG_FILE" "BASE_PATH" "PROJECT") 79 | 80 | for var_name in "${variables[@]}"; do 81 | # Using indirect reference to check the value of the variable by its name 82 | if [ -z "${!var_name+x}" ]; then 83 | echo "$var_name is unset" 84 | exit 1 85 | fi 86 | done 87 | 88 | if ! command -v aws &> /dev/null; then 89 | echo "Error: aws CLI is not installed." 90 | exit 1 91 | fi 92 | 93 | echo "validate" | aws s3 cp - "$BASE_PATH/$PROJECT/.validate" --profile s3-publish --metadata "surrogate-key=test" --cache-control "max-age=0" 94 | aws s3 rm "$BASE_PATH/$PROJECT/.validate" --profile s3-publish 95 | 96 | curl --fail-with-body -X POST -H "Fastly-Key: $FASTLY_KEY" "https://api.fastly.com/service/$FASTLY_SERVICE/purge/test" 97 | } 98 | 99 | # Call the function based on action value 100 | case $ACTION in 101 | "publish") 102 | publish 103 | ;; 104 | "verify") 105 | verify 106 | ;; 107 | # ... Add more cases as needed ... 108 | *) 109 | echo "Unknown action: $ACTION" 110 | exit 1 111 | ;; 112 | esac 113 | 114 | exit 0 115 | 116 | # bash scripts/publish.sh --action publish --version 1.0 --channel beta --assets some_asset --basePath /path/to/base -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | We are working on a feature branch basis. That means, if you want to impelement a new feature, refactor something, or 5 | add any other change, you branch you feature branch of the master branch. That is the branch on which you will do you work. 6 | The following sections will tell you how you should you feature branches, how to keep your feature branch up to date and 7 | how to write good commit messages. 8 | 9 | Feature branch mames 10 | -------------------- 11 | 12 | Branches should fulfill the following name scheme: 13 | 14 | /// 15 | 16 | For example: 17 | 18 | 2023/44/feat/disable_speed_kit_on_many_errors 19 | 2023/27/fix/implement_feature_xyz 20 | 21 | Keeping your feature branch up to date 22 | -------------------------------------- 23 | 24 | Make sure to regularily __rebase__ your feature branch onto the master branch to prevent diverging too much from it. 25 | You might miss changes which could touch the files you're working on which will result in a lot of merge conflicts. 26 | 27 | The simplest way to do that is by using IntelliJ: 28 | * Check out your feature branch 29 | * Updat the master branch 30 | * Rebase your branch onto the master branch by selecting the master branch and choosing `Rebase current onto selected` 31 | 32 | Take a look [here](https://www.jetbrains.com/help/idea/apply-changes-from-one-branch-to-another.html#rebase-branch) for more details on how to properly rebase with IntelliJ. 33 | 34 | Commit message format and content 35 | --------------------------------- 36 | 37 | Commits should have the following scheme: 38 | 39 | (): 40 | 41 | With the following values for ``: 42 | 43 | - **feat:** new feature 44 | - **fix:** bug fix 45 | - **refactor:** refactoring production code 46 | - **style:** formatting, missing semi colons, etc; no code change 47 | - **docs:** changes to documentation 48 | - **test:** adding or refactoring tests; no production code change 49 | - **chore:** updating grunt tasks etc; no production code change 50 | 51 | The `` is optional but we encourage to use them for better 52 | separation of commits within a branch. Example values could be: assets, server, build, general 53 | 54 | For example: 55 | 56 | feat(server): Implement changeOrigin on AssetAPI 57 | 58 | The commit message should contain a short explanaition what the commited change is doing. There is no need in repeating t 59 | the change itself since it's self explainatory by the code change. 60 | 61 | Please refer to [this article](https://chris.beams.io/posts/git-commit/#imperative) to to see how to write good commit messages. 62 | 63 | Run the tests locally 64 | --------------------- 65 | 66 | There are two types of tests. Before you can run the tests you need to build the project with: 67 | 68 | ```bash 69 | npm run build dist 70 | ``` 71 | 72 | The node base tests can then be started with: 73 | 74 | ```bash 75 | npm run test:node 76 | ``` 77 | 78 | The node tests can also be started with the build in mocha runner of IntelliJ. 79 | 80 | 81 | The browser based test can be used with your local installed chrome with: 82 | ```bash 83 | npm run test:browser 84 | ``` 85 | 86 | Test a specific browser engine with playwright runner use one of the following commands. 87 | Read more about it in the [offical documentation](https://modern-web.dev/docs/test-runner/browser-launchers/playwright/). 88 | 89 | ```bash 90 | npm run test:browser -- --playwright --browsers chromium 91 | npm run test:browser -- --playwright --browsers firefox 92 | npm run test:browser -- --playwright --browsers webkit 93 | ``` 94 | 95 | 96 | -------------------------------------------------------------------------------- /lib/metamodel/SetAttribute.ts: -------------------------------------------------------------------------------- 1 | import { CollectionType, PluralAttribute } from './PluralAttribute'; 2 | import { Attribute } from './Attribute'; 3 | import { Type } from './Type'; 4 | import { JsonArray, JsonMap } from '../util'; 5 | import { Managed } from '../binding'; 6 | import { ManagedState } from '../intersection'; 7 | 8 | export class SetAttribute extends PluralAttribute, T> { 9 | /** 10 | * Get the type id for this set type 11 | * @return 12 | */ 13 | static get ref(): string { 14 | return '/db/collection.Set'; 15 | } 16 | 17 | /** 18 | * @inheritDoc 19 | */ 20 | get collectionType() { 21 | return CollectionType.SET; 22 | } 23 | 24 | /** 25 | * @param name The name of the attribute 26 | * @param elementType The element type of the collection 27 | */ 28 | constructor(name: string, elementType: Type) { 29 | super(name, Set, elementType); 30 | } 31 | 32 | /** 33 | * @inheritDoc 34 | */ 35 | getJsonValue(state: ManagedState, object: Managed, 36 | options: { excludeMetadata?: boolean; depth?: number | boolean, persisting: boolean }): JsonArray | null { 37 | const value = this.getValue(object); 38 | 39 | if (!(value instanceof this.typeConstructor)) { 40 | return null; 41 | } 42 | 43 | const persisting: { [key: string]: T | null } = {}; 44 | const persistedState: JsonMap = Attribute.attachState(value) || {}; 45 | let changed = Attribute.attachSize(value) !== value.size; 46 | 47 | const json: JsonArray = []; 48 | const iter = value.values(); 49 | for (let item = iter.next(); !item.done; item = iter.next()) { 50 | const el = item.value; 51 | const jsonValue = this.elementType.toJsonValue(state, el, options); 52 | json.push(jsonValue); 53 | 54 | const keyValue = this.keyValue(jsonValue); 55 | persisting[keyValue] = el; 56 | changed = changed || persistedState[keyValue] !== el; 57 | } 58 | 59 | if (options.persisting) { 60 | Attribute.attachState(value, persisting, true); 61 | Attribute.attachSize(value, value.size); 62 | } 63 | 64 | if (changed) { 65 | state.setDirty(); 66 | } 67 | 68 | return json; 69 | } 70 | 71 | /** 72 | * @inheritDoc 73 | */ 74 | setJsonValue(state: ManagedState, object: Managed, json: JsonArray, 75 | options: { onlyMetadata?: boolean; persisting: boolean }): void { 76 | let value: Set | null = null; 77 | 78 | if (json) { 79 | value = this.getValue(object); 80 | 81 | if (!(value instanceof this.typeConstructor)) { 82 | value = new this.typeConstructor(); // eslint-disable-line new-cap 83 | } 84 | 85 | const persisting: { [keyValue: string]: T | null } = {}; 86 | const persistedState: { [keyValue: string]: T | null } = Attribute.attachState(value) || {}; 87 | 88 | value.clear(); 89 | for (let i = 0, len = json.length; i < len; i += 1) { 90 | const jsonValue = json[i]; 91 | const keyValue = this.keyValue(jsonValue); 92 | 93 | const el = this.elementType.fromJsonValue(state, jsonValue, persistedState[keyValue], options); 94 | value.add(el); 95 | 96 | persisting[keyValue] = el; 97 | } 98 | 99 | if (options.persisting) { 100 | Attribute.attachState(value, persisting, true); 101 | Attribute.attachSize(value, value.size); 102 | } 103 | } 104 | 105 | this.setValue(object, value); 106 | } 107 | 108 | /** 109 | * @inheritDoc 110 | */ 111 | toJSON() { 112 | return { 113 | type: `${SetAttribute.ref}[${this.elementType.ref}]`, 114 | ...super.toJSON(), 115 | }; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /spec/assets/test.json: -------------------------------------------------------------------------------- 1 | {"web-app": { 2 | "servlet": [ 3 | { 4 | "servlet-name": "cofaxCDS", 5 | "servlet-class": "org.cofax.cds.CDSServlet", 6 | "init-param": { 7 | "configGlossary:installationAt": "Philadelphia, PA", 8 | "configGlossary:adminEmail": "ksm@pobox.com", 9 | "configGlossary:poweredBy": "Cofax", 10 | "configGlossary:poweredByIcon": "/images/cofax.gif", 11 | "configGlossary:staticPath": "/content/static", 12 | "templateProcessorClass": "org.cofax.WysiwygTemplate", 13 | "templateLoaderClass": "org.cofax.FilesTemplateLoader", 14 | "templatePath": "templates", 15 | "templateOverridePath": "", 16 | "defaultListTemplate": "listTemplate.htm", 17 | "defaultFileTemplate": "articleTemplate.htm", 18 | "useJSP": false, 19 | "jspListTemplate": "listTemplate.jsp", 20 | "jspFileTemplate": "articleTemplate.jsp", 21 | "cachePackageTagsTrack": 200, 22 | "cachePackageTagsStore": 200, 23 | "cachePackageTagsRefresh": 60, 24 | "cacheTemplatesTrack": 100, 25 | "cacheTemplatesStore": 50, 26 | "cacheTemplatesRefresh": 15, 27 | "cachePagesTrack": 200, 28 | "cachePagesStore": 100, 29 | "cachePagesRefresh": 10, 30 | "cachePagesDirtyRead": 10, 31 | "searchEngineListTemplate": "forSearchEnginesList.htm", 32 | "searchEngineFileTemplate": "forSearchEngines.htm", 33 | "searchEngineRobotsDb": "WEB-INF/robots.db", 34 | "useDataStore": true, 35 | "dataStoreClass": "org.cofax.SqlDataStore", 36 | "redirectionClass": "org.cofax.SqlRedirection", 37 | "dataStoreName": "cofax", 38 | "dataStoreDriver": "com.microsoft.jdbc.sqlserver.SQLServerDriver", 39 | "dataStoreUrl": "jdbc:microsoft:sqlserver://LOCALHOST:1433;DatabaseName=goon", 40 | "dataStoreUser": "sa", 41 | "dataStorePassword": "dataStoreTestQuery", 42 | "dataStoreTestQuery": "SET NOCOUNT ON;select test='test';", 43 | "dataStoreLogFile": "/usr/local/tomcat/logs/datastore.log", 44 | "dataStoreInitConns": 10, 45 | "dataStoreMaxConns": 100, 46 | "dataStoreConnUsageLimit": 100, 47 | "dataStoreLogLevel": "debug", 48 | "maxUrlLength": 500}}, 49 | { 50 | "servlet-name": "cofaxEmail", 51 | "servlet-class": "org.cofax.cds.EmailServlet", 52 | "init-param": { 53 | "mailHost": "mail1", 54 | "mailHostOverride": "mail2"}}, 55 | { 56 | "servlet-name": "cofaxAdmin", 57 | "servlet-class": "org.cofax.cds.AdminServlet"}, 58 | 59 | { 60 | "servlet-name": "fileServlet", 61 | "servlet-class": "org.cofax.cds.FileServlet"}, 62 | { 63 | "servlet-name": "cofaxTools", 64 | "servlet-class": "org.cofax.cms.CofaxToolsServlet", 65 | "init-param": { 66 | "templatePath": "toolstemplates/", 67 | "log": 1, 68 | "logLocation": "/usr/local/tomcat/logs/CofaxTools.log", 69 | "logMaxSize": "", 70 | "dataLog": 1, 71 | "dataLogLocation": "/usr/local/tomcat/logs/dataLog.log", 72 | "dataLogMaxSize": "", 73 | "removePageCache": "/content/admin/remove?cache=pages&id=", 74 | "removeTemplateCache": "/content/admin/remove?cache=templates&id=", 75 | "fileTransferFolder": "/usr/local/tomcat/webapps/content/fileTransferFolder", 76 | "lookInContext": 1, 77 | "adminGroupID": 4, 78 | "betaServer": true}}], 79 | "servlet-mapping": { 80 | "cofaxCDS": "/", 81 | "cofaxEmail": "/cofaxutil/aemail/*", 82 | "cofaxAdmin": "/admin/*", 83 | "fileServlet": "/static/*", 84 | "cofaxTools": "/tools/*"}, 85 | 86 | "taglib": { 87 | "taglib-uri": "cofax.tld", 88 | "taglib-location": "/WEB-INF/tlds/cofax.tld"}}} -------------------------------------------------------------------------------- /lib/binding/Enhancer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign,max-classes-per-file */ 2 | import { Class } from '../util/Class'; 3 | import { Attribute, ManagedType } from '../metamodel'; 4 | import type { Managed } from './Managed'; 5 | 6 | const BAQEND_ID = Symbol('BaqendId'); 7 | const BAQEND_TYPE = Symbol('BaqendType'); 8 | 9 | export class Enhancer { 10 | /** 11 | * @param superClass 12 | * @return typeConstructor 13 | */ 14 | createProxy(superClass: Class): Class { 15 | return class Proxy extends (superClass as any) {} as Class; 16 | } 17 | 18 | /** 19 | * @param typeConstructor 20 | * @returns type the managed type metadata for this class 21 | */ 22 | static getBaqendType(typeConstructor: Class | Function): ManagedType | null { 23 | return (typeConstructor as any)[BAQEND_TYPE]; 24 | } 25 | 26 | /** 27 | * @param typeConstructor 28 | * @return 29 | */ 30 | static getIdentifier(typeConstructor: Class | Function): string | null { 31 | return (typeConstructor as any)[BAQEND_ID]; 32 | } 33 | 34 | /** 35 | * @param typeConstructor 36 | * @param identifier 37 | */ 38 | static setIdentifier(typeConstructor: Class, identifier: string): void { 39 | (typeConstructor as any)[BAQEND_ID] = identifier; 40 | } 41 | 42 | /** 43 | * @param type 44 | * @param typeConstructor 45 | */ 46 | enhance(type: ManagedType, typeConstructor: Class): void { 47 | if ((typeConstructor as any)[BAQEND_TYPE] === type) { 48 | return; 49 | } 50 | 51 | if (Object.prototype.hasOwnProperty.call(typeConstructor, BAQEND_TYPE)) { 52 | throw new Error('Type is already used by a different manager'); 53 | } 54 | 55 | (typeConstructor as any)[BAQEND_TYPE] = type; 56 | 57 | Enhancer.setIdentifier(typeConstructor, type.ref); 58 | this.enhancePrototype(typeConstructor.prototype, type); 59 | } 60 | 61 | /** 62 | * Enhance the prototype of the type 63 | * @param proto 64 | * @param type 65 | */ 66 | enhancePrototype(proto: T, type: ManagedType): void { 67 | if (type.isEmbeddable) { 68 | return; // we do not need to enhance the prototype of embeddable 69 | } 70 | 71 | if (proto.toString === Object.prototype.toString) { 72 | // implements a better convenience toString method 73 | Object.defineProperty(proto, 'toString', { 74 | value: function toString() { 75 | return this._metadata.id || this._metadata.bucket; 76 | }, 77 | enumerable: false, 78 | }); 79 | } 80 | 81 | // enhance all persistent object properties 82 | if (type.superType && type.superType.name === 'Object') { 83 | type.superType.declaredAttributes.forEach((attr) => { 84 | if (!attr.isMetadata) { 85 | this.enhanceProperty(proto, attr); 86 | } 87 | }); 88 | } 89 | 90 | // enhance all persistent properties 91 | type.declaredAttributes.forEach((attr) => { 92 | this.enhanceProperty(proto, attr); 93 | }); 94 | } 95 | 96 | /** 97 | * @param proto 98 | * @param attribute 99 | */ 100 | enhanceProperty(proto: T, attribute: Attribute): void { 101 | const { name } = attribute; 102 | Object.defineProperty(proto, name, { 103 | get() { 104 | this._metadata.throwUnloadedPropertyAccess(name); 105 | return null; 106 | }, 107 | set(value) { 108 | this._metadata.throwUnloadedPropertyAccess(name); 109 | Object.defineProperty(this, name, { 110 | value, 111 | writable: true, 112 | enumerable: true, 113 | configurable: true, 114 | }); 115 | }, 116 | configurable: true, 117 | enumerable: true, 118 | }); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /cli/schema.ts: -------------------------------------------------------------------------------- 1 | import { join as pathJoin } from 'path'; 2 | import { EntityManager, message, util } from 'baqend'; 3 | import inquirer from 'inquirer'; 4 | import * as account from './account'; 5 | import { AccountArgs } from './account'; 6 | import { 7 | ensureDir, isNativeClassNamespace, readDir, readFile, writeFile, 8 | } from './helper'; 9 | 10 | type JsonMap = util.JsonMap; 11 | 12 | const SCHEMA_FILE_PATH = './baqend/schema/'; 13 | 14 | export type SchemaArgs = { 15 | command: 'upload' | 'download' | 'deploy' 16 | force?: boolean, 17 | }; 18 | 19 | export async function uploadSchema(db: EntityManager, force: boolean = false): Promise { 20 | let fileNames; 21 | try { 22 | fileNames = await readDir(SCHEMA_FILE_PATH); 23 | } catch (e) { 24 | throw new Error('Your schema folder baqend is empty, no schema changes were deployed.'); 25 | } 26 | 27 | const schemas: JsonMap[] = await Promise.all( 28 | fileNames.map(async (fileName) => { 29 | const file = await readFile(pathJoin(SCHEMA_FILE_PATH, fileName), 'utf-8'); 30 | return JSON.parse(file) as JsonMap; 31 | }), 32 | ); 33 | 34 | if (force) { 35 | const { answer } = await inquirer.prompt([ 36 | { type: 'confirm', name: 'answer', message: 'This will delete ALL your App data. Are you sure you want to continue?' }, 37 | ]); 38 | 39 | if (!answer) { 40 | return false; 41 | } 42 | 43 | schemas.forEach((schemaDescriptor: JsonMap) => { 44 | console.log(`Replacing ${(schemaDescriptor.class as string).replace('/db/', '')} Schema`); 45 | }); 46 | await db.send(new message.ReplaceAllSchemas(schemas)); 47 | return true; 48 | } 49 | 50 | const acls = schemas.map((schemaDescriptor) => { 51 | console.log(`Updating ${(schemaDescriptor.class as string).replace('/db/', '')} Schema`); 52 | return { operation: 'updateClassAcl', bucket: schemaDescriptor.class, acl: schemaDescriptor.acl }; 53 | }); 54 | 55 | try { 56 | // We need to update acls separately, since they are not changed by an additive update 57 | // Deploy schemas first to ensure all schemas exists 58 | await db.send(new message.UpdateAllSchemas(schemas)); 59 | await db.send(new message.UpdateAllSchemas(acls)); 60 | return true; 61 | } catch (e: any) { 62 | console.error(`Schema update failed with error: ${e.message}`); 63 | return false; 64 | } 65 | } 66 | 67 | export function downloadSchema(db: EntityManager) { 68 | return db.send(new message.GetAllSchemas()).then((res) => Promise.all( 69 | res.entity.map((schemaDescriptor: JsonMap) => { 70 | const classname = (schemaDescriptor.class as string).replace('/db/', ''); 71 | const filename = `baqend/schema/${classname}.json`; 72 | 73 | if (!isNativeClassNamespace(classname) && classname !== 'Object') { 74 | return writeFile(filename, JSON.stringify(schemaDescriptor, null, 2), 'utf-8').then(() => { 75 | console.log(`Downloaded ${classname} Schema`); 76 | }); 77 | } 78 | return Promise.resolve(); 79 | }), 80 | )); 81 | } 82 | 83 | export function schema(args: SchemaArgs & AccountArgs) { 84 | return account.login(args).then((db) => ensureDir(SCHEMA_FILE_PATH).then(() => { 85 | switch (args.command) { 86 | case 'upload': 87 | case 'deploy': 88 | return uploadSchema(db, args.force).then((deployed) => { 89 | console.log('---------------------------------------'); 90 | if (deployed) { 91 | console.log(`The schema was successfully ${args.force ? 'replaced' : 'updated'}`); 92 | } else { 93 | console.log('The schema update was aborted'); 94 | } 95 | }); 96 | case 'download': 97 | return downloadSchema(db).then(() => { 98 | console.log('---------------------------------------'); 99 | console.log('Your schema was successfully downloaded'); 100 | }); 101 | default: 102 | throw new Error(`Invalid command: "${args.command}". Please use one of ["deploy", "download"].`); 103 | } 104 | })); 105 | } 106 | -------------------------------------------------------------------------------- /cli/copy.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { ReadStream } from 'fs'; 3 | import { EntityManager } from 'baqend'; 4 | import * as account from './account'; 5 | import { isDir, stat } from './helper'; 6 | 7 | function splitArg(arg: string): [string | null, string] { 8 | const index = arg.lastIndexOf(':'); 9 | // Has app and path part? 10 | if (index >= 0) { 11 | const app = arg.substring(0, index); 12 | const path = arg.substring(index + 1); 13 | 14 | // Add www bucket prefix if path is relative 15 | const absolutePath = path.startsWith('/') ? path : `/www/${path}`; 16 | if (!absolutePath.match(/^\/\w+\//)) { 17 | throw new Error('The path must begin with a bucket.'); 18 | } 19 | 20 | return [app, absolutePath]; 21 | } 22 | 23 | // Has local part? 24 | return [null, arg]; 25 | } 26 | 27 | function isDirectory(db: EntityManager | null, path: string): Promise { 28 | if (db) { 29 | return Promise.resolve(path.endsWith('/')); 30 | } 31 | 32 | return isDir(path); 33 | } 34 | 35 | function extractFilename(path: string): string { 36 | const index = path.lastIndexOf('/'); 37 | if (index >= 0) { 38 | return path.substring(index + 1); 39 | } 40 | 41 | return path; 42 | } 43 | 44 | function removeTrailingSlash(path: string): string { 45 | if (path.endsWith('/')) { 46 | return path.substring(0, path.length - 1); 47 | } 48 | 49 | return path; 50 | } 51 | 52 | function normalizeArgs(sourceDB: EntityManager | null, sourcePath: string, 53 | destDB: EntityManager | null, destPath: string) { 54 | return isDirectory(destDB, destPath).then((directory) => { 55 | let destination = destPath; 56 | if (directory) { 57 | const sourceFilename = extractFilename(sourcePath); 58 | destination = `${removeTrailingSlash(destPath)}/${sourceFilename}`; 59 | } 60 | 61 | return [sourcePath, destination]; 62 | }); 63 | } 64 | 65 | function login(sourceApp: string | null, destApp: string | null): 66 | Promise<[EntityManager | null, EntityManager | null]> { 67 | if (sourceApp) { 68 | if (destApp) { 69 | return Promise.all([account.login({ app: sourceApp }), account.login({ app: destApp })]); 70 | } 71 | return Promise.all([account.login({ app: sourceApp }), null]); 72 | } 73 | 74 | if (destApp) { 75 | return Promise.all([null, account.login({ app: destApp })]); 76 | } 77 | return Promise.resolve([null, null]); 78 | } 79 | 80 | function streamFrom(db: EntityManager | null, path: string): Promise<[ReadStream, number]> { 81 | if (db) { 82 | const file = new db.File({ path }); 83 | 84 | return file.loadMetadata().then(() => Promise.all([file.download({ type: 'stream' }) as Promise, file.size!])); 85 | } 86 | 87 | return stat(path).then((st) => { 88 | if (!st || !st.isFile()) { 89 | throw new Error(`${path} is not a valid file.`); 90 | } 91 | 92 | return [fs.createReadStream(path), st.size]; 93 | }); 94 | } 95 | 96 | function streamTo(db: EntityManager | null, path: string, rs: ReadStream, size: number): Promise { 97 | if (db) { 98 | const file = new db.File({ 99 | path, data: rs, size, type: 'stream', 100 | }); 101 | return file.upload({ force: true }); 102 | } 103 | 104 | return new Promise((resolve, reject) => { 105 | const ws = fs.createWriteStream(path); 106 | rs.on('end', resolve); 107 | rs.on('error', reject); 108 | 109 | rs.pipe(ws); 110 | }); 111 | } 112 | 113 | /** 114 | * Copies from arbitrary location to each other. 115 | */ 116 | export function copy(args: { source: string, dest: string }): Promise { 117 | // TODO: Split arguments with destructure in the future 118 | const [sourceApp, sourcePath] = splitArg(args.source); 119 | const [destApp, destPath] = splitArg(args.dest); 120 | 121 | return login(sourceApp, destApp) 122 | .then(([sourceDB, destDB]) => normalizeArgs(sourceDB, sourcePath, destDB, destPath) 123 | .then(([nSourcePath, nDestPath]) => streamFrom(sourceDB, nSourcePath) 124 | .then(([rs, size]) => streamTo(destDB, nDestPath, rs, size)))); 125 | } 126 | -------------------------------------------------------------------------------- /lib/metamodel/Type.ts: -------------------------------------------------------------------------------- 1 | import { Class, Json } from '../util'; 2 | import { ManagedState } from '../intersection'; 3 | 4 | export enum PersistenceType { 5 | BASIC = 0, 6 | EMBEDDABLE = 1, 7 | ENTITY = 2, 8 | MAPPED_SUPERCLASS = 3, 9 | } 10 | 11 | export abstract class Type { 12 | public readonly ref: string; 13 | 14 | public readonly name: string; 15 | 16 | // this property cant be made protected right now, since it cause a wired error in the BasicTypes class 17 | // @see #https://github.com/microsoft/TypeScript/issues/17293 18 | /* protected */ _typeConstructor?: Class; 19 | 20 | /** 21 | * The persistent type of this type 22 | */ 23 | get persistenceType(): number { 24 | return -1; 25 | } 26 | 27 | /** 28 | * @type boolean 29 | * @readonly 30 | */ 31 | get isBasic() { 32 | return this.persistenceType === PersistenceType.BASIC; 33 | } 34 | 35 | /** 36 | * @type boolean 37 | * @readonly 38 | */ 39 | get isEmbeddable() { 40 | return this.persistenceType === PersistenceType.EMBEDDABLE; 41 | } 42 | 43 | /** 44 | * @type boolean 45 | * @readonly 46 | */ 47 | get isEntity() { 48 | return this.persistenceType === PersistenceType.ENTITY; 49 | } 50 | 51 | /** 52 | * @type boolean 53 | * @readonly 54 | */ 55 | get isMappedSuperclass() { 56 | return this.persistenceType === PersistenceType.MAPPED_SUPERCLASS; 57 | } 58 | 59 | /** 60 | * The type constructor of this type 61 | */ 62 | get typeConstructor(): Class { 63 | return this._typeConstructor!!; 64 | } 65 | 66 | /** 67 | * @param typeConstructor - sets the type constructor of this type if it is not already set 68 | */ 69 | set typeConstructor(typeConstructor: Class) { 70 | if (this._typeConstructor) { 71 | throw new Error('typeConstructor has already been set.'); 72 | } 73 | this._typeConstructor = typeConstructor; 74 | } 75 | 76 | /** 77 | * @param ref 78 | * @param typeConstructor 79 | */ 80 | protected constructor(ref: string, typeConstructor?: Class) { 81 | if (ref.indexOf('/db/') !== 0) { 82 | throw new SyntaxError(`Type ref ${ref} is invalid.`); 83 | } 84 | 85 | this.ref = ref; 86 | this.name = ref.substring(4); 87 | this._typeConstructor = typeConstructor; 88 | } 89 | 90 | /** 91 | * Merge the json data into the current object instance and returns the merged object 92 | * @param state The root object state 93 | * @param jsonValue The json data to merge 94 | * @param currentValue The object where the jsonObject will be merged into, if the current object is null, 95 | * a new instance will be created 96 | * @param options additional options which are applied through the conversion 97 | * @param [options.onlyMetadata=false] Indicates that only the metadata should be updated of the object 98 | * @param [options.persisting=false] indicates if the current state will be persisted. 99 | * Used to update the internal change tracking state of collections and mark the object persistent or dirty afterwards 100 | * @return The merged object instance 101 | */ 102 | abstract fromJsonValue(state: ManagedState, jsonValue: Json, currentValue: T | null, 103 | options: { persisting: boolean, onlyMetadata?: boolean }) : T | null; 104 | 105 | /** 106 | * Converts the given object to json 107 | * @param state The root object state 108 | * @param object The object to convert 109 | * @param options additional options which are applied through the conversion 110 | * @param [options.excludeMetadata=false] Indicates that no metadata should be exposed on the generated json 111 | * @param [options.depth=0] The object depth to serialize 112 | * @param [options.persisting=false] indicates if the current state will be persisted. 113 | * Used to update the internal change tracking state of collections and mark the object persistent if its true 114 | * @return The converted object as json 115 | */ 116 | abstract toJsonValue(state: ManagedState, object: T | null, 117 | options: { excludeMetadata?: boolean, depth?: number | boolean, persisting: boolean }): Json; 118 | } 119 | -------------------------------------------------------------------------------- /lib/connector/FetchConnector.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connector, Request, Response, ResponseBodyType, 3 | } from './Connector'; 4 | import { Message } from './Message'; 5 | 6 | export class FetchConnector extends Connector { 7 | /** 8 | * Indicates if this connector implementation is usable for the given host and port 9 | * @return 10 | */ 11 | static isUsable(): boolean { 12 | return typeof fetch !== 'undefined'; 13 | } 14 | 15 | /** 16 | * @inheritDoc 17 | */ 18 | doSend(message: Message, request: Request, receive: (response: Response) => void) { 19 | const url = this.origin + this.basePath + request.path; 20 | const { method } = request; 21 | const { headers } = request; 22 | const { entity } = request; 23 | const credentials = message.withCredentials ? 'include' : 'same-origin'; 24 | 25 | return fetch(url, { 26 | method, 27 | headers, 28 | body: entity, 29 | credentials, 30 | }).then((res) => { 31 | const responseHeaders: { [headerName: string]: string } = {}; 32 | Connector.RESPONSE_HEADERS.forEach((name) => { 33 | responseHeaders[name] = res.headers.get ? res.headers.get(name) : (res.headers as any)[name]; 34 | }); 35 | 36 | const response: Response = { 37 | headers: responseHeaders, 38 | status: res.status, 39 | entity: res, 40 | }; 41 | 42 | receive(response); 43 | }); 44 | } 45 | 46 | /** 47 | * @inheritDoc 48 | */ 49 | fromFormat(response: Response, rawEntity: any, type: ResponseBodyType | null) { 50 | if (type === 'json') { 51 | return rawEntity.json(); 52 | } if (type === 'blob') { 53 | return rawEntity.blob(); 54 | } if (type === 'data-url' || type === 'base64') { 55 | return rawEntity.blob().then((entity: Blob) => { 56 | const reader = new FileReader(); 57 | reader.readAsDataURL(entity); 58 | return new Promise((resolve, reject) => { 59 | reader.onload = resolve; 60 | reader.onerror = reject; 61 | }).then(() => { 62 | let result = reader.result as string; 63 | if (type === 'base64') { 64 | result = result.substring(result.indexOf(',') + 1); 65 | } 66 | return result; 67 | }); 68 | }); 69 | } 70 | 71 | return rawEntity; 72 | } 73 | 74 | /** 75 | * @inheritDoc 76 | */ 77 | toFormat(message: Message) { 78 | let { type } = message.request; 79 | 80 | if (type) { 81 | let { entity } = message.request; 82 | let mimeType = message.mimeType(); 83 | switch (type) { 84 | case 'blob': 85 | mimeType = mimeType || entity.type; 86 | break; 87 | case 'arraybuffer': 88 | case 'form': 89 | break; 90 | case 'data-url': { 91 | const match = entity.match(/^data:(.+?)(;base64)?,(.*)$/); 92 | const isBase64 = match[2]; 93 | // eslint-disable-next-line prefer-destructuring 94 | entity = match[3]; 95 | 96 | type = 'blob'; 97 | mimeType = mimeType || match[1]; 98 | if (!isBase64) { 99 | entity = decodeURIComponent(entity); 100 | break; 101 | } 102 | } // fallthrough 103 | case 'base64': { 104 | const binaryStr = atob(entity); 105 | const len = binaryStr.length; 106 | const array = new Uint8Array(len); 107 | for (let i = 0; i < len; i += 1) { 108 | array[i] = binaryStr.charCodeAt(i); 109 | } 110 | type = 'blob'; 111 | entity = new Blob([array], { type: mimeType }); 112 | break; 113 | } 114 | case 'json': { 115 | if (typeof entity !== 'string') { 116 | entity = JSON.stringify(entity); 117 | } 118 | break; 119 | } 120 | case 'text': 121 | break; 122 | default: 123 | throw new Error(`Supported request format:${type}`); 124 | } 125 | 126 | message.entity(entity, type).mimeType(mimeType); 127 | } 128 | } 129 | } 130 | 131 | Connector.connectors.push(FetchConnector); 132 | -------------------------------------------------------------------------------- /lib/metamodel/MapAttribute.ts: -------------------------------------------------------------------------------- 1 | import { CollectionType, PluralAttribute } from './PluralAttribute'; 2 | import { Attribute } from './Attribute'; 3 | import { PersistentError } from '../error'; 4 | import { Type } from './Type'; 5 | import { Json, JsonMap } from '../util'; 6 | import { Managed } from '../binding'; 7 | import { ManagedState } from '../intersection'; 8 | 9 | export class MapAttribute extends PluralAttribute, V> { 10 | public keyType: Type; 11 | 12 | /** 13 | * Get the type id for this map type 14 | * @return 15 | */ 16 | static get ref(): string { 17 | return '/db/collection.Map'; 18 | } 19 | 20 | /** 21 | * @inheritDoc 22 | */ 23 | get collectionType() { 24 | return CollectionType.MAP; 25 | } 26 | 27 | /** 28 | * @param name 29 | * @param keyType 30 | * @param elementType 31 | */ 32 | constructor(name: string, keyType: Type, elementType: Type) { 33 | super(name, Map, elementType); 34 | this.keyType = keyType; 35 | } 36 | 37 | /** 38 | * @inheritDoc 39 | */ 40 | getJsonValue(state: ManagedState, object: Managed, 41 | options: { excludeMetadata?: boolean; depth?: number | boolean; persisting: boolean }): Json | undefined { 42 | const value = this.getValue(object); 43 | 44 | if (!(value instanceof this.typeConstructor)) { 45 | return null; 46 | } 47 | 48 | const persisting: { [key: string]: [K | null, V | null] } = {}; 49 | const persistedState: { [key: string]: [K | null, V | null] } = Attribute.attachState(value) || {}; 50 | let changed = Attribute.attachSize(value) !== value.size; 51 | 52 | const json: JsonMap = {}; 53 | const iter = value.entries(); 54 | for (let el = iter.next(); !el.done; el = iter.next()) { 55 | const entry = el.value; 56 | 57 | if (entry[0] === null || entry[0] === undefined) { 58 | throw new PersistentError('Map keys can\'t be null nor undefined.'); 59 | } 60 | 61 | const jsonKey = this.keyValue(this.keyType.toJsonValue(state, entry[0], options)); 62 | json[jsonKey] = this.elementType.toJsonValue(state, entry[1], options); 63 | 64 | persisting[jsonKey] = [entry[0], entry[1]]; 65 | changed = changed || (persistedState[jsonKey] || [])[1] !== entry[1]; 66 | } 67 | 68 | if (options.persisting) { 69 | Attribute.attachState(value, persisting, true); 70 | Attribute.attachSize(value, value.size); 71 | } 72 | 73 | if (changed) { 74 | state.setDirty(); 75 | } 76 | 77 | return json; 78 | } 79 | 80 | /** 81 | * @inheritDoc 82 | */ 83 | setJsonValue(state: ManagedState, object: Managed, json: JsonMap, 84 | options: { onlyMetadata?: boolean; persisting: boolean }): void { 85 | let value: Map | null = null; 86 | 87 | if (json) { 88 | value = this.getValue(object); 89 | 90 | if (!(value instanceof this.typeConstructor)) { 91 | // eslint-disable-next-line new-cap 92 | value = new this.typeConstructor(); 93 | } 94 | 95 | const persisting: { [key: string]: [K | null, V | null] } = {}; 96 | const persistedState: { [key: string]: [K | null, V | null] } = Attribute.attachState(value) || {}; 97 | 98 | value.clear(); 99 | const jsonKeys = Object.keys(json); 100 | for (let i = 0, len = jsonKeys.length; i < len; i += 1) { 101 | const jsonKey = jsonKeys[i]; 102 | const persistedEntry = persistedState[jsonKey] || []; 103 | // ensures that "false" keys will be converted to false, disallow null as keys 104 | const key = this.keyType.fromJsonValue(state, jsonKey, persistedEntry[0], options); 105 | const val = this.elementType.fromJsonValue(state, json[jsonKey], persistedEntry[1], options); 106 | 107 | persisting[jsonKey] = [key, val]; 108 | value.set(key, val); 109 | } 110 | 111 | if (options.persisting) { 112 | Attribute.attachState(value, persisting, true); 113 | Attribute.attachSize(value, value.size); 114 | } 115 | } 116 | 117 | this.setValue(object, value); 118 | } 119 | 120 | /** 121 | * @inheritDoc 122 | */ 123 | toJSON() { 124 | return { 125 | type: `${MapAttribute.ref}[${this.keyType.ref},${this.elementType.ref}]`, 126 | ...super.toJSON(), 127 | }; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | 4 | plugins: [ 5 | '@typescript-eslint', 6 | 'node', 7 | ], 8 | 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:import/recommended', 12 | 'plugin:@typescript-eslint/eslint-recommended', 13 | // "plugin:@typescript-eslint/recommended", 14 | // 'plugin:node/recommended-module', 15 | ], 16 | 17 | env: { 18 | node: true, 19 | }, 20 | 21 | ignorePatterns: ['dist/**', 'doc/**', 'commonjs/**', 'lib/**/*.js', 'cli/**/*.js', 'oauth.js'], 22 | 23 | rules: { 24 | // enforce consistent linebreak style 25 | // https://eslint.org/docs/rules/linebreak-style 26 | 'linebreak-style': 'off', 27 | 28 | strict: ['error', 'global'], 29 | 30 | // specify the maximum length of a line in your program 31 | // http://eslint.org/docs/rules/max-len 32 | 'max-len': ['error', 120, 2, { 33 | ignoreUrls: true, 34 | ignoreComments: false, 35 | ignoreRegExpLiterals: true, 36 | ignoreStrings: true, 37 | ignoreTemplateLiterals: true, 38 | }], 39 | 40 | // we are using unused args in several interface definitions 41 | 'no-unused-vars': 'off', 42 | // we do not need that, since we are using ts 43 | 'import/no-unresolved': 'off', 44 | }, 45 | 46 | overrides: [ 47 | { 48 | files: ['lib/**/*.ts', 'connect.js'], 49 | parserOptions: { 50 | sourceType: 'module', 51 | ecmaVersion: 2015, 52 | project: './tsconfig.json', 53 | }, 54 | 55 | env: { 56 | es6: true, 57 | browser: true, 58 | node: true, 59 | }, 60 | 61 | rules: { 62 | // we disallow default exports 63 | "import/prefer-default-export": "off", 64 | "import/no-default-export": "error", 65 | 66 | // we use this in several places, should be discussed if we want to re-enable this 67 | 'class-methods-use-this': 'off', 68 | "no-underscore-dangle": ['error', { "allowAfterThis": true }], 69 | 70 | 'node/no-deprecated-api': 'error', 71 | 'no-buffer-constructor': 'error', 72 | 'no-console': 'error', 73 | } 74 | }, 75 | 76 | { 77 | files: ['cli/*.ts'], 78 | parserOptions: { 79 | sourceType: 'module', 80 | ecmaVersion: 2015, 81 | project: './tsconfig.json', 82 | }, 83 | 84 | env: { 85 | node: true, 86 | }, 87 | 88 | rules: { 89 | 'no-console': 'off', 90 | '@typescript-eslint/no-use-before-define': 'off', 91 | // we must require our self which isn't supported right now in ts 92 | // https://github.com/microsoft/TypeScript/issues/38675 93 | 'import/no-extraneous-dependencies': 'off', 94 | 'no-restricted-syntax': 'off', 95 | } 96 | }, 97 | 98 | { 99 | files: ['spec/*.js'], 100 | 101 | globals: { 102 | Abort: true, 103 | ArrayBuffer: true, 104 | DB: true, 105 | env: true, 106 | expect: true, 107 | helper: true, 108 | Map: true, 109 | Promise: true, 110 | Set: true, 111 | Baqend: true, 112 | }, 113 | 114 | env: { 115 | es6: false, 116 | node: true, 117 | mocha: true, 118 | browser: true, 119 | }, 120 | 121 | rules: { 122 | 'consistent-return': 'off', 123 | 'func-names': 'off', 124 | 'global-require': 'off', 125 | 'guard-for-in': 'warn', 126 | 'new-cap': 'warn', 127 | 'no-console': 'warn', 128 | 'no-empty': ['error', { allowEmptyCatch: true }], 129 | 'no-eval': 'warn', 130 | 'no-mixed-operators': 'off', 131 | 'no-multi-str': 'off', 132 | 'no-new': 'off', 133 | 'no-restricted-syntax': 'off', 134 | 'no-shadow': 'warn', 135 | 'no-unused-expressions': 'off', 136 | 'no-unused-vars': 'warn', 137 | 'no-use-before-define': 'warn', 138 | 'no-var': 'off', 139 | 'node/no-unpublished-require': 'off', 140 | 'object-shorthand': 'off', 141 | 'one-var': 'off', 142 | 'one-var-declaration-per-line': 'off', 143 | 'prefer-arrow-callback': 'off', 144 | 'prefer-promise-reject-errors': 'warn', 145 | 'vars-on-top': 'off', 146 | '@typescript-eslint/no-unused-expressions': 'off', 147 | 'no-await-in-loop': 'off', 148 | }, 149 | }, 150 | ], 151 | }; 152 | -------------------------------------------------------------------------------- /lib/intersection/PushMessage.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '../binding'; 2 | import type * as model from '../model'; 3 | import type { Json, JsonMap } from '../util'; 4 | 5 | export interface PushMessageOptions { 6 | /** 7 | * The icon of the push message 8 | */ 9 | icon?: string; 10 | /** 11 | * The badge for iOS or Web Push devices 12 | */ 13 | badge?: string | number; 14 | /** 15 | * The number for iOS and Android devices which will occur on the top right of 16 | * the icon 17 | */ 18 | nativeBadge?: number; 19 | /** 20 | * The web badge is the small monochrome icon which will occur on small devices 21 | * (web push only) 22 | */ 23 | webBadge?: string; 24 | /** 25 | * An image of the push message (web push only) 26 | */ 27 | image?: string; 28 | /** 29 | * Actions that the user can invoke and interact with (web push only) 30 | */ 31 | actions?: JsonMap; 32 | /** 33 | * Defines which direction the text should be displayed (web push only) 34 | */ 35 | dir?: string; 36 | /** 37 | * The sound of an incoming push message (web push only) 38 | */ 39 | sound?: string; 40 | /** 41 | * The tag of the push message where messages are going to be collected (web push only) 42 | */ 43 | tag?: string; 44 | /** 45 | * The vibrate property specifies a vibration pattern for the device's 46 | * vibration 47 | */ 48 | vibrate?: number[]; 49 | /** 50 | * The renotify option makes new notifications vibrate and play a sound 51 | * (web push only) 52 | */ 53 | renotify?: boolean; 54 | /** 55 | * The requireInteraction option lets stay the push message until the 56 | * user interacts with it (web push only) 57 | */ 58 | requireInteraction?: boolean; 59 | /** 60 | * The silent option shows a new notification but prevents default behavior 61 | * (web push only) 62 | */ 63 | silent?: boolean; 64 | /** 65 | * The data object which can contain additional information. 66 | */ 67 | data?: Json; 68 | } 69 | 70 | // Extends all push message options 71 | export interface PushMessage extends PushMessageOptions {} 72 | 73 | /** 74 | * PushMessages are used to send a push notification to a set of devices 75 | */ 76 | export class PushMessage { 77 | /** 78 | * Set of devices 79 | */ 80 | public devices: Set; 81 | 82 | /** 83 | * Push notification message 84 | */ 85 | public message?: string; 86 | 87 | /** 88 | * Push notification subject 89 | */ 90 | public subject?: string; 91 | 92 | /** 93 | * Push message will be used to send a push notification to a set of devices 94 | * 95 | * @param [devices] The Set of device references which 96 | * will receive this push notification. 97 | * @param message The message of the push notification. 98 | * @param subject The subject of the push notification. 99 | * @param [options] The options object which can contain additional information and data 100 | * @param [badge] The badge for iOS or Web Push devices 101 | * @param [data] The data object which can contain additional information. 102 | */ 103 | constructor(devices: model.Device | Set | Iterable, message?: string, subject?: string, 104 | options?: string | PushMessageOptions, badge?: string | number, data?: Json) { 105 | const opts = typeof options === 'string' ? { sound: options, badge, data } : (options || {}); 106 | 107 | this.devices = PushMessage.initDevices(devices); 108 | this.message = message; 109 | this.subject = subject; 110 | 111 | Object.assign(this, opts); 112 | } 113 | 114 | /** 115 | * Instantiates a set of devices from the given parameter 116 | * @param devices 117 | * @return 118 | */ 119 | private static initDevices(devices?: model.Device | Set | Iterable): Set { 120 | if (devices instanceof Set) { 121 | return devices; 122 | } 123 | 124 | if (devices instanceof Entity) { 125 | return new Set([devices]); 126 | } 127 | 128 | if (!devices || devices[Symbol.iterator]) { 129 | return new Set(devices); 130 | } 131 | 132 | throw new Error('Only Sets, Lists and Arrays can be used as devices.'); 133 | } 134 | 135 | /** 136 | * Adds a new object to the set of devices 137 | * @param device will be added to the device set to receive the push notification 138 | * @return 139 | */ 140 | addDevice(device: model.Device): void { 141 | this.devices.add(device); 142 | } 143 | 144 | /** 145 | * Converts the push message to JSON 146 | * @return 147 | */ 148 | toJSON(): JsonMap { 149 | if (!this.devices || !this.devices.size) { 150 | throw new Error('Set of devices is empty.'); 151 | } 152 | 153 | return Object.assign({} as JsonMap, this, { 154 | devices: Array.from(this.devices, (device) => device.id), 155 | }); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "baqend", 3 | "version": "4.0.0", 4 | "description": "Baqend JavaScript SDK", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Baqend.com", 8 | "email": "info@baqend.com" 9 | }, 10 | "homepage": "https://www.baqend.com", 11 | "main": "./commonjs/lib/index.js", 12 | "module": "./dist/baqend.es2015.js", 13 | "browser": { 14 | ".": "./dist/baqend.es5.js", 15 | "./lib/util/atob.ts": "./lib/util/atob-browser.ts", 16 | "./lib/util/is-node.ts": "./lib/util/is-node-browser.ts", 17 | "./lib/util/hmac.ts": "./lib/util/hmac-browser.ts", 18 | "./lib/connector/NodeConnector": false, 19 | "./lib/connector/FetchConnector": false 20 | }, 21 | "react-native": { 22 | ".": "./commonjs/lib/index.js", 23 | "./commonjs/lib/util/atob.js": "./commonjs/lib/util/atob-rn.js", 24 | "./commonjs/lib/util/is-node.js": "./commonjs/lib/util/is-node-browser.js", 25 | "./commonjs/lib/util/hmac.js": "./commonjs/lib/util/hmac-browser.js", 26 | "./commonjs/lib/connector/XMLHttpConnector": false, 27 | "./commonjs/lib/connector/IFrameConnector": false 28 | }, 29 | "exports": { 30 | "./cli": "./cli/index.js", 31 | ".": { 32 | "browser": { 33 | "import": "./dist/baqend.es2015.js", 34 | "require": "./dist/baqend.es5.js" 35 | }, 36 | "default": { 37 | "import": "./esm/lib/index.js", 38 | "require": "./commonjs/lib/index.js" 39 | } 40 | } 41 | }, 42 | "types": "./commonjs/lib/index.d.ts", 43 | "dependencies": { 44 | "commander": "^7.2.0", 45 | "glob": "^7.2.3", 46 | "inquirer": "^8.2.5", 47 | "open": "^8.4.2", 48 | "rimraf": "^3.0.2", 49 | "tslib": "^2.6.0", 50 | "uuid": "^8.3.2", 51 | "validator": "^13.9.0" 52 | }, 53 | "repository": { 54 | "type": "git", 55 | "url": "https://github.com/Baqend/js-sdk.git" 56 | }, 57 | "bugs": { 58 | "url": "https://github.com/Baqend/js-sdk/issues" 59 | }, 60 | "keywords": [ 61 | "backend", 62 | "baqend", 63 | "mongodb", 64 | "sdk", 65 | "backend-as-a-service" 66 | ], 67 | "devDependencies": { 68 | "@purtuga/esm-webpack-plugin": "^1.5.0", 69 | "@types/glob": "^8.1.0", 70 | "@types/inquirer": "^8.2.6", 71 | "@types/node": "^16.18.38", 72 | "@types/uuid": "^8.3.4", 73 | "@types/validator": "^13.7.17", 74 | "@types/webpack": "^4.41.33", 75 | "@typescript-eslint/eslint-plugin": "^6.5.0", 76 | "@typescript-eslint/parser": "^6.5.0", 77 | "@web/test-runner": "^0.17.0", 78 | "@web/test-runner-browserstack": "^0.6.1", 79 | "@web/test-runner-junit-reporter": "^0.6.1", 80 | "@web/test-runner-playwright": "^0.10.1", 81 | "baqend": "file:.", 82 | "chai": "^4.3.8", 83 | "eslint": "^8.48.0", 84 | "eslint-plugin-import": "^2.28.1", 85 | "eslint-plugin-node": "^11.1.0", 86 | "mocha": "^10.2.0", 87 | "mocha-junit-reporter": "^2.2.1", 88 | "otpauth": "^9.2.1", 89 | "shelljs": "^0.8.4", 90 | "shx": "^0.3.4", 91 | "terser-webpack-plugin": "^3.1.0", 92 | "ts-loader": "^8.4.0", 93 | "ts-node": "^10.9.1", 94 | "typedoc": "^0.23.28", 95 | "typescript": "^4.9.5", 96 | "utils": "^0.2.2", 97 | "webpack": "^4.47.0", 98 | "webpack-cli": "^4.10.0" 99 | }, 100 | "scripts": { 101 | "test": "npm run test:node && npm run test:browser", 102 | "test:node": "mocha --config mocharc.cjs --exit", 103 | "test:browser": "web-test-runner", 104 | "test:browserstack": "web-test-runner --config web-test-runner.bs.config.mjs", 105 | "test:playwright": "web-test-runner --config web-test-runner.pw.config.mjs", 106 | "clean": "shx rm -rf dist commonjs doc", 107 | "dev-node-commonjs": "tsc --watch -m commonjs --target es5 --declaration --outDir commonjs --declaration --project tsconfig.lib.json", 108 | "_dev-node-esm": "UNSUDED FOR NOW -> https://nodejs.org/api/packages.html#packages_dual_package_hazard tsc --watch -m es2015 --target es6 --outDir esm --project tsconfig.lib.json", 109 | "build-commonjs": "tsc -m commonjs --target es5 --declaration --outDir commonjs --project tsconfig.lib.json", 110 | "_build-esm": "UNSUDED FOR NOW -> https://nodejs.org/api/packages.html#packages_dual_package_hazard tsc -m es2015 --target es6 --outDir esm --project tsconfig.lib.json", 111 | "build-cli": "tsc -m commonjs --target es6 --project tsconfig.cli.json", 112 | "build-web-bundles": "webpack --config-name=es5 && webpack --config-name=es2015", 113 | "dist": "npm link . && npm run build-commonjs && npm run build-web-bundles && npm run build-cli && npm run docs", 114 | "prepack": "npm run dist", 115 | "typings": "npm run typings:test", 116 | "typings:test": "tsc --version && tsc -p spec-ts/tsconfig.json --noEmit", 117 | "docs": "typedoc --out doc lib", 118 | "docs-actian": "typedoc --readme README-ACTIAN.md --out doc lib", 119 | "baqend": "npm run build-cli && node cli", 120 | "lint": "eslint ." 121 | }, 122 | "engines": { 123 | "node": ">=16.0.0" 124 | }, 125 | "bin": { 126 | "baqend": "./cli/index.js" 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /lib/GeoPoint.ts: -------------------------------------------------------------------------------- 1 | import { JsonMap } from './util'; 2 | 3 | /** 4 | * Creates a new GeoPoint instance 5 | * From latitude and longitude 6 | * From a json object 7 | * Or an tuple of latitude and longitude 8 | */ 9 | export class GeoPoint { 10 | /** 11 | * How many radians fit in one degree. 12 | */ 13 | static readonly DEG_TO_RAD = Math.PI / 180; 14 | 15 | /** 16 | * The Earth radius in kilometers used by {@link GeoPoint#kilometersTo} 17 | */ 18 | static readonly EARTH_RADIUS_IN_KILOMETERS = 6371; 19 | 20 | /** 21 | * The Earth radius in miles used by {@link GeoPoint#milesTo} 22 | */ 23 | static readonly EARTH_RADIUS_IN_MILES = 3956; 24 | 25 | /** 26 | * Longitude of the given point 27 | */ 28 | public longitude: number; 29 | 30 | /** 31 | * Latitude of the given point 32 | */ 33 | public latitude: number; 34 | 35 | /** 36 | * Creates a GeoPoint with the user's current location, if available. 37 | * @return A promise that will be resolved with a GeoPoint 38 | */ 39 | static current(): Promise { 40 | return new Promise(((resolve, reject) => { 41 | if (!navigator) { 42 | reject(new Error('This seems not to be a browser context.')); 43 | } 44 | if (!navigator.geolocation) { 45 | reject(new Error('This browser does not support geolocation.')); 46 | } 47 | 48 | navigator.geolocation.getCurrentPosition((location) => { 49 | resolve(new GeoPoint(location.coords.latitude, location.coords.longitude)); 50 | }, (error) => { 51 | reject(error); 52 | }); 53 | })); 54 | } 55 | 56 | /** 57 | * @param latitude A coordinate pair (latitude first), 58 | * a GeoPoint like object or the GeoPoint's latitude 59 | * @param longitude The GeoPoint's longitude 60 | */ 61 | constructor(latitude?: number | string | { latitude: number, longitude: number } | [number, number], 62 | longitude?: number) { 63 | let lat: number; 64 | let lng: number; 65 | if (typeof latitude === 'string') { 66 | const index = latitude.indexOf(';'); 67 | lat = Number(latitude.substring(0, index)); 68 | lng = Number(latitude.substring(index + 1)); 69 | } else if (Array.isArray(latitude)) { 70 | [lat, lng] = latitude; 71 | } else if (typeof latitude === 'object') { 72 | lat = latitude.latitude; 73 | lng = latitude.longitude; 74 | } else { 75 | lat = typeof latitude === 'number' ? latitude : 0; 76 | lng = typeof longitude === 'number' ? longitude : 0; 77 | } 78 | 79 | this.longitude = lng; 80 | this.latitude = lat; 81 | 82 | if (this.latitude < -90 || this.latitude > 90) { 83 | throw new Error(`Latitude ${this.latitude} is not in bound of -90 <= latitude <= 90`); 84 | } 85 | 86 | if (this.longitude < -180 || this.longitude > 180) { 87 | throw new Error(`Longitude ${this.longitude} is not in bound of -180 <= longitude <= 180`); 88 | } 89 | } 90 | 91 | /** 92 | * Returns the distance from this GeoPoint to another in kilometers. 93 | * @param point another GeoPoint 94 | * @return The distance in kilometers 95 | * 96 | * @see GeoPoint#radiansTo 97 | */ 98 | kilometersTo(point: GeoPoint): number { 99 | return Number((GeoPoint.EARTH_RADIUS_IN_KILOMETERS * this.radiansTo(point)).toFixed(3)); 100 | } 101 | 102 | /** 103 | * Returns the distance from this GeoPoint to another in miles. 104 | * @param point another GeoPoint 105 | * @return The distance in miles 106 | * 107 | * @see GeoPoint#radiansTo 108 | */ 109 | milesTo(point: GeoPoint): number { 110 | return Number((GeoPoint.EARTH_RADIUS_IN_MILES * this.radiansTo(point)).toFixed(3)); 111 | } 112 | 113 | /** 114 | * Computes the arc, in radian, between two WGS-84 positions. 115 | * 116 | * The haversine formula implementation is taken from: 117 | * {@link http://www.movable-type.co.uk/scripts/latlong.html} 118 | * 119 | * Returns the distance from this GeoPoint to another in radians. 120 | * @param point another GeoPoint 121 | * @return the arc, in radian, between two WGS-84 positions 122 | * 123 | * @see http://en.wikipedia.org/wiki/Haversine_formula 124 | */ 125 | radiansTo(point: GeoPoint): number { 126 | const from = this; 127 | const to = point; 128 | const rad1 = from.latitude * GeoPoint.DEG_TO_RAD; 129 | const rad2 = to.latitude * GeoPoint.DEG_TO_RAD; 130 | const dLng = (to.longitude - from.longitude) * GeoPoint.DEG_TO_RAD; 131 | 132 | return Math.acos((Math.sin(rad1) * Math.sin(rad2)) + (Math.cos(rad1) * Math.cos(rad2) * Math.cos(dLng))); 133 | } 134 | 135 | /** 136 | * A String representation in latitude, longitude format 137 | * @return The string representation of this class 138 | */ 139 | toString(): string { 140 | return `${this.latitude};${this.longitude}`; 141 | } 142 | 143 | /** 144 | * Returns a JSON representation of the GeoPoint 145 | * @return A GeoJson object of this GeoPoint 146 | */ 147 | toJSON(): JsonMap { 148 | return { latitude: this.latitude, longitude: this.longitude }; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /lib/caching/BloomFilter.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-bitwise,default-case,no-fallthrough */ 2 | 3 | import { atob } from '../util'; 4 | 5 | /** 6 | * A Bloom Filter is a client-side kept cache sketch of the server cache 7 | */ 8 | export class BloomFilter { 9 | /** 10 | * The raw bytes of this Bloom filter. 11 | * @type string 12 | * @readonly 13 | */ 14 | public readonly bytes: string; 15 | 16 | /** 17 | * The amount of bits. 18 | * @type number 19 | * @readonly 20 | */ 21 | public readonly bits: number; 22 | 23 | /** 24 | * The amount of hashes. 25 | * @type number 26 | * @readonly 27 | */ 28 | public readonly hashes: number; 29 | 30 | /** 31 | * The creation timestamp of this bloom filter. 32 | * @type number 33 | * @readonly 34 | */ 35 | public readonly creation: number; 36 | 37 | /** 38 | * @param bloomFilter The raw Bloom filter. 39 | * @param bloomFilter.m The raw Bloom filter bits. 40 | * @param bloomFilter.h The raw Bloom filter hashes. 41 | * @param bloomFilter.b The Base64-encoded raw Bloom filter bytes. 42 | */ 43 | constructor(bloomFilter: { m: number, h: number, b: string }) { 44 | this.bytes = atob(bloomFilter.b); 45 | this.bits = bloomFilter.m; 46 | this.hashes = bloomFilter.h; 47 | this.creation = Date.now(); 48 | } 49 | 50 | /** 51 | * Returns whether this Bloom filter contains the given element. 52 | * 53 | * @param element The element to check if it is contained. 54 | * @return True, if the element is contained in this Bloom filter. 55 | */ 56 | contains(element: string): boolean { 57 | const hashes = BloomFilter.getHashes(element, this.bits, this.hashes); 58 | for (let i = 0, len = hashes.length; i < len; i += 1) { 59 | if (!this.isSet(hashes[i])) { 60 | return false; 61 | } 62 | } 63 | return true; 64 | } 65 | 66 | /** 67 | * Checks whether a bit is set at a given position. 68 | * 69 | * @param index The position index to check. 70 | * @return True, if the bit is set at the given position. 71 | */ 72 | private isSet(index: number): boolean { 73 | const pos = Math.floor(index / 8); 74 | const bit = 1 << (index % 8); 75 | // Extract byte as int or NaN if out of range 76 | const byte = this.bytes.charCodeAt(pos); 77 | // Bit-wise AND should be non-zero (NaN always yields false) 78 | return (byte & bit) !== 0; 79 | } 80 | 81 | /** 82 | * Returns the hases of a given element in the Bloom filter. 83 | * 84 | * @param element The element to check. 85 | * @param bits The amount of bits. 86 | * @param hashes The amount of hashes. 87 | * @return The hashes of an element in the Bloom filter. 88 | */ 89 | private static getHashes(element: string, bits: number, hashes: number): number[] { 90 | const hashValues = new Array(hashes); 91 | const hash1 = BloomFilter.murmur3(0, element); 92 | const hash2 = BloomFilter.murmur3(hash1, element); 93 | for (let i = 0; i < hashes; i += 1) { 94 | hashValues[i] = (hash1 + (i * hash2)) % bits; 95 | } 96 | return hashValues; 97 | } 98 | 99 | /** 100 | * Calculate a Murmur3 hash. 101 | * 102 | * @param seed A seed to use for the hashing. 103 | * @param key A key to check. 104 | * @return A hashed value of key. 105 | */ 106 | private static murmur3(seed: number, key: string): number { 107 | const remainder = key.length & 3; 108 | const bytes = key.length - remainder; 109 | const c1 = 0xcc9e2d51; 110 | const c2 = 0x1b873593; 111 | let h1; 112 | let h1b; 113 | let k1; 114 | let i; 115 | h1 = seed; 116 | i = 0; 117 | 118 | while (i < bytes) { 119 | k1 = ((key.charCodeAt(i) & 0xff)) 120 | | ((key.charCodeAt(i += 1) & 0xff) << 8) 121 | | ((key.charCodeAt(i += 1) & 0xff) << 16) 122 | | ((key.charCodeAt(i += 1) & 0xff) << 24); 123 | i += 1; 124 | 125 | k1 = ((((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16))) & 0xffffffff; 126 | k1 = (k1 << 15) | (k1 >>> 17); 127 | k1 = ((((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16))) & 0xffffffff; 128 | 129 | h1 ^= k1; 130 | h1 = (h1 << 13) | (h1 >>> 19); 131 | h1b = ((((h1 & 0xffff) * 5) + ((((h1 >>> 16) * 5) & 0xffff) << 16))) & 0xffffffff; 132 | h1 = (((h1b & 0xffff) + 0x6b64) + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16)); 133 | } 134 | 135 | k1 = 0; 136 | 137 | switch (remainder) { 138 | case 3: 139 | k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16; 140 | case 2: 141 | k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8; 142 | case 1: 143 | k1 ^= (key.charCodeAt(i) & 0xff); 144 | 145 | k1 = (((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff; 146 | k1 = (k1 << 15) | (k1 >>> 17); 147 | k1 = (((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff; 148 | h1 ^= k1; 149 | } 150 | 151 | h1 ^= key.length; 152 | 153 | h1 ^= h1 >>> 16; 154 | h1 = (((h1 & 0xffff) * 0x85ebca6b) + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff; 155 | h1 ^= h1 >>> 13; 156 | h1 = ((((h1 & 0xffff) * 0xc2b2ae35) + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16))) & 0xffffffff; 157 | h1 ^= h1 >>> 16; 158 | 159 | return h1 >>> 0; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /lib/connector/IFrameConnector.ts: -------------------------------------------------------------------------------- 1 | /* this connector will only be choose in browser compatible environments */ 2 | /* eslint no-restricted-globals: ["off", "location", "addEventListener"] */ 3 | 4 | import { Connector, Receiver, Request } from './Connector'; 5 | import { XMLHttpConnector } from './XMLHttpConnector'; 6 | import { Message } from './Message'; 7 | import { JsonMap } from '../util'; 8 | 9 | export class IFrameConnector extends XMLHttpConnector { 10 | public static readonly style = 'width:1px;height:1px;position:absolute;top:-10px;left:-10px;'; 11 | 12 | private mid: number; 13 | 14 | private messages: { [messageId: number]: Receiver }; 15 | 16 | private iframe?: HTMLIFrameElement; 17 | 18 | private queue: any[] | null = null; 19 | 20 | private connected: boolean = false; 21 | 22 | /** 23 | * Indicates if this connector implementation is usable for the given host and port 24 | * @param host 25 | * @param port 26 | * @param secure 27 | * @return 28 | */ 29 | static isUsable(host: string, port: number, secure: boolean): boolean { 30 | // we use location directly here, since there exists environments, which provide a location and a document but 31 | // no window object 32 | if (typeof location === 'undefined' || typeof document === 'undefined') { 33 | return false; 34 | } 35 | 36 | const locationSecure = location.protocol === 'https:'; 37 | const locationPort = location.port || (locationSecure ? 443 : 80); 38 | 39 | return location.hostname !== host || locationPort !== port || locationSecure !== secure; 40 | } 41 | 42 | constructor(host: string, port: number, secure: boolean, basePath: string) { 43 | super(host, port, secure, basePath); 44 | 45 | this.mid = 0; 46 | this.messages = {}; 47 | this.doReceive = this.doReceive.bind(this); 48 | 49 | addEventListener('message', this.doReceive, false); 50 | } 51 | 52 | load(message: Message) { 53 | const url = this.origin + this.basePath + message.path(); 54 | const name = `baqend-sdk-connect-${Math.floor(Math.random() * 100000)}`; 55 | 56 | this.iframe = document.createElement('iframe'); 57 | this.iframe.name = name; 58 | this.iframe.setAttribute('style', IFrameConnector.style); 59 | document.body.appendChild(this.iframe); 60 | 61 | const form = document.createElement('form'); 62 | form.target = name; 63 | form.method = 'post'; 64 | form.action = url; 65 | form.setAttribute('style', IFrameConnector.style); 66 | 67 | const token = message.tokenStorage()?.token; 68 | if (token) { 69 | const input = document.createElement('input'); 70 | input.type = 'hidden'; 71 | input.name = 'BAT'; 72 | input.value = token; 73 | form.appendChild(input); 74 | } 75 | document.body.appendChild(form); 76 | 77 | this.queue = []; 78 | this.iframe.addEventListener('load', this.onLoad.bind(this), false); 79 | 80 | form.submit(); 81 | } 82 | 83 | onLoad() { 84 | if (!this.queue) { 85 | return; 86 | } 87 | 88 | const { queue } = this; 89 | 90 | for (let i = 0; i < queue.length; i += 1) { 91 | this.postMessage(queue[i]); 92 | } 93 | 94 | this.queue = null; 95 | } 96 | 97 | /** 98 | * @inheritDoc 99 | */ 100 | doSend(message: Message, request: Request, receive: Receiver) { 101 | // binary data will be send and received directly 102 | if (message.isBinary) { 103 | super.doSend(message, request, receive); 104 | return; 105 | } 106 | 107 | if (!this.iframe) { 108 | // ensure that we get a local resource cache hit 109 | // eslint-disable-next-line no-param-reassign 110 | message.request.path = '/connect'; 111 | this.load(message); 112 | } 113 | 114 | const msg = { 115 | mid: this.mid += 1, 116 | method: request.method, 117 | path: request.path, 118 | headers: request.headers, 119 | entity: request.entity, 120 | responseHeaders: Connector.RESPONSE_HEADERS, 121 | }; 122 | 123 | this.messages[msg.mid] = receive; 124 | 125 | const strMsg = JSON.stringify(msg); 126 | if (this.queue) { 127 | this.queue.push(strMsg); 128 | } else { 129 | this.postMessage(strMsg); 130 | } 131 | 132 | if (!this.connected) { 133 | setTimeout(() => { 134 | if (this.messages[msg.mid]) { 135 | delete this.messages[msg.mid]; 136 | receive({ 137 | status: 0, 138 | error: new Error('Connection refused.'), 139 | headers: {}, 140 | }); 141 | } 142 | }, 10000); 143 | } 144 | } 145 | 146 | postMessage(msg: string) { 147 | this.iframe!.contentWindow!.postMessage(msg, this.origin); 148 | } 149 | 150 | doReceive(event: MessageEvent) { 151 | if (event.origin !== this.origin || event.data[0] !== '{') { 152 | return; 153 | } 154 | 155 | const msg = JSON.parse(event.data) as JsonMap; 156 | 157 | const receive = this.messages[msg.mid as number]; 158 | if (receive) { 159 | delete this.messages[msg.mid as number]; 160 | this.connected = true; 161 | 162 | receive({ 163 | status: msg.status as number, 164 | headers: msg.headers as { [headerNames: string]: string }, 165 | entity: msg.entity as any, 166 | }); 167 | } 168 | } 169 | } 170 | 171 | Connector.connectors.push(IFrameConnector); 172 | -------------------------------------------------------------------------------- /lib/intersection/TokenStorage.ts: -------------------------------------------------------------------------------- 1 | import { hmac } from '../util'; 2 | import type { GlobalStorage } from './GlobalStorage'; 3 | import type { WebStorage } from './WebStorage'; 4 | 5 | export interface TokenData { 6 | val: any; 7 | sig: string; 8 | createdAt: number; 9 | data: string; 10 | expireAt: number; 11 | } 12 | 13 | export interface TokenStorageFactory { 14 | /** 15 | * Creates a new tokenStorage which persist tokens for the given origin 16 | * @param origin The origin where the token contains to 17 | * @return The initialized token storage 18 | */ 19 | create(origin: string): Promise 20 | } 21 | 22 | export class TokenStorage { 23 | static GLOBAL: typeof GlobalStorage; 24 | 25 | static WEB_STORAGE: typeof WebStorage; 26 | 27 | static hmac = hmac; 28 | 29 | /** 30 | * The actual stored token 31 | */ 32 | tokenData: TokenData | null; 33 | 34 | /** 35 | * The origin of the token 36 | */ 37 | origin: string; 38 | 39 | /** 40 | * Indicates if the token should keep temporary only or should be persisted for later sessions 41 | */ 42 | temporary: boolean; 43 | 44 | /** 45 | * Parse a token string in its components 46 | * @param token The token string to parse, time values are returned as timestamps 47 | * @return The parsed token data 48 | */ 49 | static parse(token: string): TokenData { 50 | return { 51 | val: token, 52 | createdAt: parseInt(token.substring(0, 8), 16) * 1000, 53 | expireAt: parseInt(token.substring(8, 16), 16) * 1000, 54 | sig: token.substring(token.length - 40), 55 | data: token.substring(0, token.length - 40), 56 | }; 57 | } 58 | 59 | /** 60 | * Get the stored token 61 | * @return The token or undefined, if no token is available 62 | */ 63 | get token(): string { 64 | return this.tokenData ? this.tokenData.val : null; 65 | } 66 | 67 | static create(origin: string) { 68 | return Promise.resolve(new TokenStorage(origin)); 69 | } 70 | 71 | /** 72 | * @param origin The origin where the token belongs to 73 | * @param token The initial token 74 | * @param temporary If the token should be saved temporary or permanently 75 | */ 76 | constructor(origin: string, token?: string | null, temporary?: boolean) { 77 | this.tokenData = token ? TokenStorage.parse(token) : null; 78 | this.origin = origin; 79 | this.temporary = !!temporary; 80 | } 81 | 82 | /** 83 | * Use the underlying storage implementation to save the token 84 | * @param origin The origin where the token belongs to 85 | * @param token The initial token 86 | * @param temporary If the token should be saved temporary or permanently 87 | * @return 88 | */ 89 | protected saveToken(origin: string, token: string | null, temporary: boolean): void { 90 | // eslint-disable-next-line no-underscore-dangle 91 | if (this._saveToken !== TokenStorage.prototype._saveToken) { 92 | // eslint-disable-next-line no-console 93 | console.log('Using deprecated TokenStorage._saveToken implementation.'); 94 | // eslint-disable-next-line no-underscore-dangle 95 | this._saveToken(origin, token, temporary); 96 | } 97 | } 98 | 99 | /** 100 | * Use the underlying storage implementation to save the token 101 | * @param origin The origin where the token belongs to 102 | * @param token The initial token 103 | * @param temporary If the token should be saved temporary or permanently 104 | * @return 105 | * @deprecated Use TokenStorage#saveToken instead 106 | * @protected 107 | */ 108 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 109 | protected _saveToken(origin: string, token: string | null, temporary: boolean): void {} 110 | 111 | /** 112 | * Update the token for the givin origin, the operation may be asynchronous 113 | * @param token The token to store or null to remove the token 114 | */ 115 | update(token: string | null) { 116 | const t = token ? TokenStorage.parse(token) : null; 117 | if (this.tokenData && t && this.tokenData.expireAt > t.expireAt) { 118 | // an older token was fetched from the cache, so ignore it 119 | return; 120 | } 121 | 122 | this.tokenData = t; 123 | this.saveToken(this.origin, token, this.temporary); 124 | } 125 | 126 | /** 127 | * Derives a resource token from the stored origin token and signs the resource with the generated resource token 128 | * 129 | * @param resource The resource which will be accessible with the returned token 130 | * @param sign Sign the given resource with a token, if sign is false the resource will only be encoded to a path 131 | * @return A resource token which can only be used to access the specified resource 132 | */ 133 | signPath(resource: string, sign: boolean = true): Promise { 134 | const { tokenData } = this; 135 | const result = Promise.resolve(resource.split('/').map(encodeURIComponent).join('/')); 136 | 137 | if (!tokenData || !sign) { 138 | return result; 139 | } 140 | 141 | return result.then((path) => TokenStorage.hmac(path + tokenData.data, tokenData.sig) 142 | .then((hash) => `${path}?BAT=${tokenData.data + hash}`)) 143 | .catch((e) => { 144 | // eslint-disable-next-line no-console 145 | console.warn('Can\'t sign the resource, run the SDK on a secured origin, or provide an alternative hmac implementation on TokenStorage.hmac', e); 146 | return result; 147 | }); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /lib/intersection/Validator.ts: -------------------------------------------------------------------------------- 1 | import validator from 'validator'; 2 | import { Entity } from '../binding'; 3 | import { ManagedType } from '../metamodel'; 4 | 5 | const FallBachValLib = {}; 6 | let valLib: Partial = FallBachValLib; 7 | try { 8 | // we load this module as an optional external dependency 9 | // eslint-disable-next-line global-require 10 | valLib = require('validator'); 11 | } catch (e) { 12 | // ignore loading optional module error 13 | } 14 | 15 | type Validators = Omit< 16 | typeof validator, // Extract, 17 | 'version' | 'blacklist' | 'escape' | 'unescape' | 'ltrim' | 'normalizeEmail' | 'rtrim' | 'stripLow' | 'toBoolean' | 'toDate' | 'toFloat' | 'toInt' | 'trim' | 'whitelist' | 'toString' 18 | >; 19 | 20 | // import all validators from the validation library 21 | export interface Validator extends Pick{} 22 | export class Validator { 23 | /** 24 | * Compiles the given validation code for the managedType 25 | * @param managedType The managedType of the code 26 | * @param validationCode The validation code 27 | * @return the parsed validation function 28 | */ 29 | static compile(managedType: ManagedType, validationCode: string): Function { 30 | const keys: string[] = []; 31 | const iter = managedType.attributes(); 32 | for (let el = iter.next(); !el.done; el = iter.next()) { 33 | const attr = el.value; 34 | keys.push(attr.name); 35 | } 36 | 37 | // eslint-disable-next-line @typescript-eslint/no-implied-eval,no-new-func 38 | const fn = new Function(...keys, validationCode); 39 | return function onValidate(argObj: { [arg: string]: Validator }) { 40 | if (valLib === FallBachValLib) { 41 | throw new Error('Validation code will not be executed. Make sure that the validator package is correctly provided as an external dependency.'); 42 | } 43 | 44 | const args = keys.map((name) => argObj[name]); 45 | return fn.apply({}, args); 46 | }; 47 | } 48 | 49 | /** 50 | * The cached errors of the validation 51 | */ 52 | private errors: string[] = []; 53 | 54 | /** 55 | * Entity to get the value of the attribute 56 | */ 57 | private entity: Entity; 58 | 59 | /** 60 | * Name of the attribute 61 | */ 62 | key: string; 63 | 64 | /** 65 | * Gets the value of the attribute 66 | * @return Value 67 | */ 68 | get value(): any { 69 | return this.entity[this.key]; 70 | } 71 | 72 | /** 73 | * Checks if the attribute is valid 74 | * @return 75 | */ 76 | get isValid(): boolean { 77 | return this.errors.length === 0; 78 | } 79 | 80 | /** 81 | * Executes the given validation function to validate the value. 82 | * 83 | * The value will be passed as the first parameter to the validation function and 84 | * the library {@link https://github.com/chriso/validator.js} as the second one. 85 | * If the function returns true the value is valid, otherwise it's invalid. 86 | * 87 | * @param fn will be used to validate the value 88 | * @return 89 | */ 90 | is(fn: Function): Validator; 91 | 92 | /** 93 | * Executes the given validation function to validate the value. 94 | * 95 | * The value will be passed as the first parameter to the validation function and 96 | * the library {@link https://github.com/chriso/validator.js} as the second one. 97 | * If the function returns true the value is valid, otherwise it's invalid. 98 | * 99 | * @param error The error message which will be used if the value is invalid 100 | * @param fn will be used to validate the value 101 | * @return 102 | */ 103 | is(error: string, fn: Function): Validator; 104 | 105 | is(error: string | Function, fn?: Function): Validator { 106 | if (error instanceof Function) { 107 | return this.is('is', error); 108 | } 109 | 110 | if (fn!(this.value, valLib) === false) { 111 | this.errors.push(error); 112 | } 113 | 114 | return this; 115 | } 116 | 117 | constructor(key: string, entity: Entity) { 118 | this.key = key; 119 | this.entity = entity; 120 | } 121 | 122 | callMethod(method: keyof typeof validator, errorMessage: string | null, argumentList: any[]) { 123 | const args = argumentList || []; 124 | try { 125 | args.unshift(this.toStringValue()); 126 | if ((valLib[method] as Function).apply(this, args) === false) { 127 | this.errors.push(errorMessage || method); 128 | } 129 | } catch (e: any) { 130 | this.errors.push(errorMessage || e.message); 131 | } 132 | return this; 133 | } 134 | 135 | toStringValue() { 136 | const { value } = this; 137 | if (typeof value === 'string' || value instanceof Date) { 138 | return value; 139 | } 140 | 141 | return JSON.stringify(value); 142 | } 143 | 144 | toJSON() { 145 | return { 146 | isValid: this.isValid, 147 | errors: this.errors, 148 | }; 149 | } 150 | } 151 | 152 | const OTHER_VALIDATORS: string[] = ['contains', 'equals', 'matches']; 153 | (Object.keys(valLib) as (keyof Validators)[]).forEach((name: (keyof Validators)) => { 154 | if (name.startsWith('is') || OTHER_VALIDATORS.includes(name)) { 155 | // use function here to keep the correct this context 156 | (Validator.prototype[name] as any) = function validate(this: Validator, ...args: any[]) { 157 | const error = typeof args[0] === 'string' ? args.shift() : null; 158 | return this.callMethod(name, error, args); 159 | }; 160 | } 161 | }); 162 | -------------------------------------------------------------------------------- /lib/intersection/Permission.ts: -------------------------------------------------------------------------------- 1 | import type * as model from '../model'; 2 | import type { JsonMap } from '../util'; 3 | 4 | export type TrustedEntity = model.User | model.Role | string; 5 | export type BasePermission = ['load', 'update', 'delete', 'query', 'insert']; 6 | 7 | /** 8 | * An aggregation of access rules for given object metadata. 9 | */ 10 | export class Permission { 11 | static readonly BASE_PERMISSIONS: BasePermission = ['load', 'update', 'delete', 'query', 'insert']; 12 | 13 | public rules: { [ref: string]: string } = {}; 14 | 15 | /** 16 | * Returns a list of user and role references of all rules 17 | * @return a list of references 18 | */ 19 | allRules(): string[] { 20 | return Object.keys(this.rules); 21 | } 22 | 23 | /** 24 | * Removes all rules from this permission object 25 | * @return 26 | */ 27 | clear(): void { 28 | this.rules = {}; 29 | } 30 | 31 | /** 32 | * Copies permissions from another permission object 33 | * @param permission The permission to copy from 34 | * @return 35 | */ 36 | copy(permission: Permission): Permission { 37 | this.rules = { ...permission.rules }; 38 | return this; 39 | } 40 | 41 | /** 42 | * Gets whenever all users and roles have the permission to perform the operation 43 | * @return true If public access is allowed 44 | */ 45 | isPublicAllowed(): boolean { 46 | if ('*' in this.rules) { 47 | return false; 48 | } 49 | 50 | return !this.allRules().some((ref) => this.rules[ref] === 'allow'); 51 | } 52 | 53 | /** 54 | * Sets whenever all users and roles should have the permission to perform the operation 55 | * 56 | * Note: All other allow rules will be removed. 57 | * 58 | * @return 59 | */ 60 | setPublicAllowed(): void { 61 | this.allRules().forEach((ref) => { 62 | if (this.rules[ref] === 'allow') { 63 | delete this.rules[ref]; 64 | } 65 | }); 66 | } 67 | 68 | /** 69 | * Returns the actual rule of the given user or role. 70 | * @param userOrRole The user or role to check for 71 | * @return The actual access rule or undefined if no rule was found 72 | */ 73 | getRule(userOrRole: TrustedEntity): string { 74 | return this.rules[this.ref(userOrRole)]; 75 | } 76 | 77 | /** 78 | * Checks whenever the user or role is explicit allowed to perform the operation. 79 | * 80 | * @param userOrRole The user or role to check for 81 | * @return true If the given user or role is allowed 82 | */ 83 | isAllowed(userOrRole: TrustedEntity): boolean { 84 | return this.rules[this.ref(userOrRole)] === 'allow'; 85 | } 86 | 87 | /** 88 | * Checks whenever the user or role is explicit denied to perform the operation. 89 | * 90 | * @param userOrRole The user or role to check for 91 | * @return true If the given user or role is denied 92 | */ 93 | isDenied(userOrRole: TrustedEntity): boolean { 94 | return this.rules[this.ref(userOrRole)] === 'deny'; 95 | } 96 | 97 | /** 98 | * Allows the given users or rules to perform the operation 99 | * @param userOrRole The users or roles to allow 100 | * @return this permission object 101 | */ 102 | allowAccess(...userOrRole: TrustedEntity[]): Permission { 103 | for (let i = 0; i < userOrRole.length; i += 1) { 104 | this.rules[this.ref(userOrRole[i])] = 'allow'; 105 | } 106 | 107 | return this; 108 | } 109 | 110 | /** 111 | * Denies the given users or rules to perform the operation 112 | * @param userOrRole The users or roles to deny 113 | * @return this permission object 114 | */ 115 | denyAccess(...userOrRole: TrustedEntity[]): Permission { 116 | for (let i = 0; i < userOrRole.length; i += 1) { 117 | this.rules[this.ref(userOrRole[i])] = 'deny'; 118 | } 119 | 120 | return this; 121 | } 122 | 123 | /** 124 | * Deletes any allow/deny rules for the given users or roles 125 | * @param userOrRole The users or roles to delete rules for 126 | * @return this permission object 127 | */ 128 | deleteAccess(...userOrRole: TrustedEntity[]): Permission { 129 | for (let i = 0; i < userOrRole.length; i += 1) { 130 | delete this.rules[this.ref(userOrRole[i])]; 131 | } 132 | 133 | return this; 134 | } 135 | 136 | /** 137 | * A Json representation of the set of rules 138 | * @return 139 | */ 140 | toJSON(): JsonMap { 141 | return { ...this.rules }; 142 | } 143 | 144 | /** 145 | * Sets the permission rules from json 146 | * @param json The permission json representation 147 | * @return 148 | */ 149 | fromJSON(json: JsonMap) { 150 | this.rules = { ...json } as { [ref: string]: string }; 151 | } 152 | 153 | /** 154 | * Creates a permission from the given rules. 155 | * @param json The rules. 156 | * @return The permission. 157 | */ 158 | static fromJSON(json: JsonMap): Permission { 159 | const permission = new this(); 160 | permission.fromJSON(json); 161 | return permission; 162 | } 163 | 164 | /** 165 | * Resolves user and role references and validate given references 166 | * @param userOrRole The user, role or reference 167 | * @return The resolved and validated reference 168 | */ 169 | private ref(userOrRole: TrustedEntity): string { 170 | const ref = typeof userOrRole === 'string' ? userOrRole : userOrRole.id!; 171 | 172 | if (ref.indexOf('/db/User/') === 0 || ref.indexOf('/db/Role/') === 0) { 173 | return ref; 174 | } 175 | 176 | throw new TypeError('The given object isn\'t a user, role or a valid reference.'); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /spec/helper.js: -------------------------------------------------------------------------------- 1 | var fs, http, https, urlParser; 2 | if (typeof module !== 'undefined') { 3 | fs = require('fs'); 4 | http = require('http'); 5 | https = require('https'); 6 | urlParser = require('url'); 7 | } 8 | 9 | // expose legacy exports for the current test bench 10 | if (typeof window !== 'undefined') { 11 | window.DB = Baqend.db; 12 | } else { 13 | global.DB = Baqend.db; 14 | } 15 | 16 | var rootTokenPromise; 17 | 18 | var helper = { 19 | get rootTokenStorage() { 20 | if (!rootTokenPromise) { 21 | rootTokenPromise = (async () => { 22 | var tokenStorage = new DB.util.TokenStorage(); 23 | var emf = new DB.EntityManagerFactory({ host: env.TEST_SERVER, tokenStorage }); 24 | 25 | const em = await emf.createEntityManager(true).ready(); 26 | await em.User.login('root', 'root'); 27 | return tokenStorage; 28 | })(); 29 | 30 | rootTokenPromise.catch((e) => { 31 | console.error('Root hook failed with error', e); 32 | }); 33 | } 34 | return rootTokenPromise; 35 | }, 36 | async ensureGlobalConnected() { 37 | if (DB.connection) return; 38 | 39 | const localDb = await DB.connect(env.TEST_SERVER); 40 | expect(localDb).equal(DB); 41 | }, 42 | makeLogin: function () { 43 | var text = ''; 44 | var possible = 'abcdefghijklmnopqrstuvwxyz0123456789'; 45 | 46 | for (var i = 0; i < 10; i += 1) { text += possible.charAt(Math.floor(Math.random() * possible.length)); } 47 | 48 | return `user-${text}`; 49 | }, 50 | randomize: function (name) { 51 | var rnd = Math.floor(Math.random() * 1000000); 52 | return `${name}_random_${rnd}`; 53 | }, 54 | sleep: function (time, value) { 55 | return new Promise(function (success) { 56 | setTimeout(function () { 57 | success(value); 58 | }, time); 59 | }); 60 | }, 61 | asset: async function (src, type) { 62 | if (fs) { 63 | const file = await helper.file(`spec/assets/${src}`); 64 | if (type === 'arraybuffer') { 65 | return file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength); 66 | } 67 | if (type === 'text') { 68 | return file.toString(); 69 | } 70 | if (typeof Blob !== 'undefined' && type === 'blob') { 71 | return new Blob([file.buffer], { type: 'image/png' }) 72 | } 73 | return file; 74 | } 75 | 76 | return helper.req(`/spec/assets/${src}`, type); 77 | }, 78 | file: function (path) { 79 | return new Promise(function (success, error) { 80 | fs.readFile(path, function (err, data) { 81 | if (err) error(err); 82 | success(data); 83 | }); 84 | }); 85 | }, 86 | req: function (url, responseType) { 87 | return new Promise(function (resolve, reject) { 88 | if (urlParser) { 89 | // eslint-disable-next-line node/no-deprecated-api 90 | var options = urlParser.parse(url); 91 | // If urlParse.parse() is called on Node.js, a single quote character is converted to %27, 92 | // which normally should not happen 93 | options.href = options.href.replace(/%27/g, '\''); 94 | options.path = options.path.replace(/%27/g, '\''); 95 | options.pathname = options.pathname.replace(/%27/g, '\''); 96 | 97 | options.method = 'GET'; 98 | var ht = options.protocol === 'http:' ? http : https; 99 | var req = ht.request(options, function (res) { 100 | var chunks = []; 101 | res.on('data', function (chunk) { 102 | chunks.push(chunk); 103 | }); 104 | res.on('end', function () { 105 | if (res.statusCode >= 400) { 106 | reject(new Error({ status: res.statusCode })); 107 | } else { 108 | resolve(Buffer.concat(chunks)); 109 | } 110 | }); 111 | }); 112 | req.on('error', reject); 113 | req.end(); 114 | } else { 115 | var oReq = new XMLHttpRequest(); 116 | oReq.open('GET', url, true); 117 | oReq.responseType = responseType || 'blob'; 118 | oReq.onload = function () { 119 | if (oReq.status >= 400) { 120 | reject(new Error({ status: oReq.status })); 121 | } else { 122 | resolve(oReq.response); 123 | } 124 | }; 125 | oReq.onerror = reject; 126 | oReq.send(); 127 | } 128 | }); 129 | }, 130 | isNode: typeof window === 'undefined', 131 | isPhantomJS: typeof navigator !== 'undefined' && navigator.userAgent.indexOf('PhantomJS') !== -1, 132 | isIE: typeof navigator !== 'undefined' && navigator.userAgent.indexOf('Trident') !== -1, 133 | isIE11: typeof navigator !== 'undefined' && navigator.userAgent.indexOf('Trident/7.0') !== -1, 134 | isIEdge: typeof navigator !== 'undefined' && navigator.userAgent.indexOf('Edge') !== -1, 135 | isChromium: typeof navigator !== 'undefined' && navigator.userAgentData?.brands?.some(data => data.brand === 'Chromium'), 136 | isFirefox: typeof navigator !== 'undefined' && navigator.userAgent.indexOf('Firefox') !== -1, 137 | }; 138 | 139 | helper.isWebKit = typeof navigator !== 'undefined' && !(helper.isIE || helper.isIEdge || helper.isChromium || helper.isFirefox) 140 | 141 | if (typeof module !== 'undefined') { 142 | module.exports = helper; 143 | } 144 | 145 | if (typeof window !== 'undefined' && '__WTR_CONFIG__' in window) { 146 | // register mocha root hook for the web test runner based on the github issue 147 | // https://github.com/modernweb-dev/web/issues/1462#issue-895453629 148 | // window.__WTR_CONFIG__.testFrameworkConfig.rootHooks = helper.mochaHooks; 149 | } 150 | -------------------------------------------------------------------------------- /spec/emf.spec.js: -------------------------------------------------------------------------------- 1 | if (typeof module !== 'undefined') { 2 | require('./node'); 3 | } 4 | 5 | describe('Test EntityManagerFactory', function () { 6 | var emf, model = [ 7 | { 8 | class: '/db/TestClass', 9 | fields: { 10 | testValue: { 11 | name: 'testValue', 12 | type: '/db/Integer', 13 | }, 14 | }, 15 | }, 16 | ]; 17 | 18 | beforeEach(function () { 19 | emf = new DB.EntityManagerFactory(); 20 | }); 21 | 22 | it('should connect to destination', function () { 23 | expect(emf.isReady).be.false; 24 | emf.connect(env.TEST_SERVER); 25 | 26 | var em = emf.createEntityManager(); 27 | return em.ready().then(function () { 28 | expect(em.isReady).be.true; 29 | }); 30 | }); 31 | 32 | it('should reject connect to invalid destination', function () { 33 | if (helper.isFirefox) { 34 | // FF can't connect to unresolvable destinations in playwright container 35 | return this.skip(); 36 | } 37 | 38 | this.timeout(40000); 39 | 40 | expect(emf.isReady).be.false; 41 | 42 | var connectPromise = emf.connect('http://example.local/'); 43 | var em = emf.createEntityManager(); 44 | 45 | return Promise.all([ 46 | connectPromise.then(function () { 47 | expect(true).be.false; 48 | }, function (e) { 49 | expect(e).instanceOf(Error); 50 | expect(emf.isReady).be.false; 51 | }), 52 | 53 | emf.ready().then(function () { 54 | expect(true).be.false; 55 | }, function (e) { 56 | expect(e).instanceOf(Error); 57 | expect(emf.isReady).be.false; 58 | }), 59 | 60 | em.ready().then(function () { 61 | expect(true).be.false; 62 | }, function (e) { 63 | expect(e).instanceOf(Error); 64 | expect(em.isReady).be.false; 65 | }), 66 | ]); 67 | }); 68 | 69 | it('should create ems before and after connect', function () { 70 | var ready = false; 71 | 72 | var em1 = emf.createEntityManager(); 73 | expect(em1.isReady).be.false; 74 | var ready1 = em1.ready().then(function () { 75 | expect(em1.isReady).be.true; 76 | expect(ready).be.true; 77 | }); 78 | 79 | expect(emf.isReady).be.false; 80 | emf.connect(env.TEST_SERVER); 81 | 82 | var em2 = emf.createEntityManager(); 83 | expect(em1.isReady).be.false; 84 | expect(em2.isReady).be.false; 85 | var ready2 = em2.ready().then(function () { 86 | expect(em2.isReady).be.true; 87 | expect(ready).be.true; 88 | }); 89 | 90 | emf.ready().then(function () { 91 | ready = true; 92 | }); 93 | 94 | return Promise.all([ready1, ready2]); 95 | }); 96 | 97 | it('should create ems after connect', function () { 98 | emf.connect(env.TEST_SERVER); 99 | 100 | var ready = false; 101 | 102 | var em1 = emf.createEntityManager(); 103 | expect(em1.isReady).be.false; 104 | var ready1 = em1.ready().then(function () { 105 | expect(em1.isReady).be.true; 106 | expect(ready).be.true; 107 | }); 108 | 109 | var em2 = emf.createEntityManager(); 110 | expect(em2.isReady).be.false; 111 | var ready2 = em2.ready().then(function () { 112 | expect(em2.isReady).be.true; 113 | expect(ready).be.true; 114 | }); 115 | 116 | emf.ready().then(function () { 117 | ready = true; 118 | }); 119 | 120 | return Promise.all([ready1, ready2]); 121 | }); 122 | 123 | it('should create async em after connect', function () { 124 | emf.connect(env.TEST_SERVER); 125 | 126 | var ready = false; 127 | emf.ready().then(function () { 128 | ready = true; 129 | }); 130 | 131 | var em1 = emf.createEntityManager(); 132 | expect(em1.isReady).be.false; 133 | var ready1 = em1.ready().then(function () { 134 | expect(em1.isReady).be.true; 135 | expect(ready).be.true; 136 | }); 137 | 138 | var ready2 = emf.ready().then(function () { 139 | var em2 = emf.createEntityManager(); 140 | expect(em2.isReady).be.true; 141 | expect(ready).be.true; 142 | }); 143 | 144 | return Promise.all([ready1, ready2]); 145 | }); 146 | 147 | it('should create ems when immediately connected', function () { 148 | var emf = new DB.EntityManagerFactory(env.TEST_SERVER); 149 | 150 | var ready = false; 151 | 152 | var em1 = emf.createEntityManager(); 153 | expect(em1.isReady).be.false; 154 | var ready1 = em1.ready().then(function () { 155 | expect(em1.isReady).be.true; 156 | expect(ready).be.true; 157 | }); 158 | 159 | var em2 = emf.createEntityManager(); 160 | expect(em2.isReady).be.false; 161 | var ready2 = em2.ready().then(function () { 162 | expect(em2.isReady).be.true; 163 | expect(ready).be.true; 164 | }); 165 | 166 | emf.ready().then(function () { 167 | ready = true; 168 | }); 169 | 170 | return Promise.all([ready1, ready2]); 171 | }); 172 | 173 | it('should create async em when immediately connected', function () { 174 | var emf = new DB.EntityManagerFactory(env.TEST_SERVER); 175 | 176 | var ready = false; 177 | emf.ready().then(function () { 178 | ready = true; 179 | }); 180 | 181 | var em1 = emf.createEntityManager(); 182 | expect(em1.isReady).be.false; 183 | var ready1 = em1.ready().then(function () { 184 | expect(em1.isReady).be.true; 185 | expect(ready).be.true; 186 | }); 187 | 188 | var ready2 = emf.ready().then(function () { 189 | var em2 = emf.createEntityManager(); 190 | expect(em2.isReady).be.true; 191 | expect(ready).be.true; 192 | }); 193 | 194 | return Promise.all([ready1, ready2]); 195 | }); 196 | }); 197 | -------------------------------------------------------------------------------- /cli/index.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | import program from 'commander'; 4 | import * as account from './account'; 5 | import { typings } from './typings'; 6 | import { schema } from './schema'; 7 | import { download } from './download'; 8 | import { deploy } from './deploy'; 9 | import { copy } from './copy'; 10 | 11 | export { deploy, typings, account }; 12 | 13 | const pjson = require('../package.json'); 14 | 15 | export function run() { 16 | program 17 | .name('baqend') 18 | .version(pjson.version) 19 | .option('--token ', 'Pass a Baqend Authorization Token to the command'); 20 | program 21 | .command('login [app]') 22 | .option('--auth ', 'The authentication method to use for the login. Can be password, google, facebook or gitHub.') 23 | .description('Logs you in and locally saves your credentials') 24 | .action((app, options) => 25 | account.persistLogin({ app, ...options, ...program.opts() }).then(credentials => { 26 | if (credentials) { 27 | console.log('You have successfully been logged in.') 28 | } 29 | })); 30 | program 31 | .command('register') 32 | .description('Registers an account and locally saves your credentials') 33 | .action((options) => account.register({ ...options, ...program.opts() }).then(() => { })); 34 | program 35 | .command('whoami [app]') 36 | .alias('me') 37 | .description('Show your login status') 38 | .action((app) => account.whoami({ app, ...program.opts() })); 39 | program 40 | .command('open [app]') 41 | .description('Opens the url to your app') 42 | .action((app) => account.openApp(app).then(() => { })); 43 | program 44 | .command('dashboard') 45 | .description('Opens the url to the baqend dashboard') 46 | .action((options) => account.openDashboard({ ...options, ...program.opts() })); 47 | program 48 | .command('deploy [app]') 49 | .description('Deploys your Baqend code and files') 50 | .option('-F, --files', 'deploy files') 51 | .option('-f, --file-dir ', 'path to file directory', 'www') 52 | .option('-g, --file-glob ', 'pattern to match files', '**/*') 53 | .option('-b, --bucket-path ', 'remote path where the files will be uploaded to.', 'www') 54 | .option('-B, --cretae-bucket', 'create the bucket, if it does not exists.') 55 | .option('-C, --code', 'deploy baqend code') 56 | .option('-c, --code-dir ', 'path to code directory', 'baqend') 57 | .option('-S, --schema', 'deploy schema') 58 | .action((app, options) => deploy({ app, ...options, ...program.opts() }).then(() => { })); 59 | program 60 | .command('copy ') 61 | .alias('cp') 62 | .description('Copies single files to and from Baqend') 63 | .usage(`[OPTIONS] SRC_PATH DEST_PATH 64 | copy|cp [OPTIONS] APP:SRC_PATH DEST_PATH 65 | copy|cp [OPTIONS] SRC_PATH APP:DEST_PATH 66 | copy|cp [OPTIONS] APP:SRC_PATH APP:DEST_PATH`) 67 | .action((source, dest, options) => copy({ 68 | source, dest, ...options, ...program.opts(), 69 | })) 70 | .on('--help', () => { 71 | console.log(` 72 | You can specify local paths without colon and app paths with a colon. 73 | For APP, you can use either your Baqend app's name or an API endpoint: "https://example.org/v1". 74 | If the app path is relative, it is assumed you are using the "www" bucket: 75 | 76 | baqend cp my-app:index.html . 77 | 78 | Is the same as: 79 | 80 | baqend cp my-app:/www/index.html . 81 | 82 | If you target a directory, the filename of the source file will be used. 83 | You can also copy files between different apps, or between community editions and apps.`); 84 | }); 85 | program 86 | .command('download [app]') 87 | .description('Downloads your Baqend code and files') 88 | .option('-C, --code', 'download code') 89 | .option('-c, --code-dir ', 'path to code directory', 'baqend') 90 | .action((app, options) => download({ app, ...options, ...program.opts() }).then(() => { })); 91 | program 92 | .command('schema [app]') 93 | .description('Upload and download your schema') 94 | .option('-F, --force', 'overwrite old schema') 95 | .action((command, app, options) => schema({ 96 | command, app, ...options, ...program.opts(), 97 | })); 98 | 99 | // program 100 | // .command('schema download [app]') 101 | // .action((app, options) => result = schema.download(Object.assign({app: app}, options))) 102 | 103 | program 104 | .command('logout [app]') 105 | .description('Removes your stored credentials') 106 | .action((app) => account.logout({ app, ...program.opts() })); 107 | program 108 | .command('typings ') 109 | .description('Generates additional typings (TypeScript support)') 110 | .option('-d, --dest ', 'The destination where the typings should be saved', '.') 111 | .action((app, options) => typings({ app, ...options, ...program.opts() })); 112 | program 113 | .usage('[command] [options] ') 114 | .description( 115 | 'Type in one of the above commands followed by --help to get more information\n' 116 | + ' The optional [app] parameter can be passed to define the target of a command.\n' 117 | + ' It can be either an app name or a custom domain location like\n' 118 | + ' https://my-baqend-domain:8080/v1.', 119 | ); 120 | program 121 | .command('apps') 122 | .description('List all your apps') 123 | .action((options) => account.listApps({ ...options, ...program.opts() })); 124 | 125 | return program.parseAsync(process.argv); 126 | } 127 | 128 | if (require.main === module) { 129 | run().catch((e) => { 130 | console.error(e.stack || e); 131 | process.exit(1); 132 | }); 133 | } 134 | -------------------------------------------------------------------------------- /spec/pushnotification.spec.js: -------------------------------------------------------------------------------- 1 | if (typeof module !== 'undefined') { 2 | require('./node'); 3 | } 4 | 5 | describe('Test Push Notifications', function () { 6 | var emf, db, lock; 7 | 8 | var TEST_GCM_DEVICE = 'APA91bFBRJGMI2OkQxhV3peP4ncZOIxGJBJ8s0tkKyWvzQErpZmuSzMzm6ugz3rOauMQ1CRui0bBsEQvuN0W8X1wTP547C6MSNcErnNYXyvc1F5eKZCs-GAtE_NcESolea2AM6_cRe9R'; 9 | var TEST_GCM_APIKEY = 'AAAAiHTmunA:APA91bF91CeP1L9QjhrxI2VTQpcf2L39CZY1zBragj4KwUiuXgZYfu4IKtT_S5he1sIHINkunGWpQEo1bsHLbWdTrKUW2Op7ykUBn9JCMKjYrgjUxwPbRyFudxd-ouz3TuYynKQa8xX0'; 10 | 11 | before(async function () { 12 | this.timeout(40000); 13 | 14 | var retires = 0; 15 | emf = new DB.EntityManagerFactory({ 16 | host: env.TEST_SERVER, 17 | tokenStorage: await helper.rootTokenStorage, 18 | staleness: 0, 19 | }); 20 | 21 | await emf.ready(); 22 | 23 | if (!emf.metamodel.entity('Lock')) { 24 | var Lock = new DB.metamodel.EntityType('Lock', emf.metamodel.entity(Object)); 25 | emf.metamodel.addType(Lock); 26 | await emf.metamodel.save(Lock); 27 | } 28 | 29 | db = emf.createEntityManager(); 30 | lock = new db.Lock({ id: 'push' }); 31 | await createLock(); 32 | 33 | var msg = new DB.message.GCMAKey(); 34 | msg.entity(TEST_GCM_APIKEY, 'text'); 35 | await emf.send(msg); 36 | 37 | try { 38 | await db.Device.loadWebPushKey(); 39 | } catch { 40 | await emf.send(new DB.message.VAPIDKeys()); 41 | } 42 | 43 | function createLock() { 44 | return lock.insert() 45 | .catch(function (e) { 46 | retires += 1; 47 | if (retires > 60) { 48 | throw e; 49 | } 50 | return helper.sleep(500) 51 | .then(createLock); 52 | }); 53 | } 54 | }); 55 | 56 | after(function () { 57 | return lock.delete(); 58 | }); 59 | 60 | beforeEach(function () { 61 | db = emf.createEntityManager(); 62 | return db.ready(); 63 | }); 64 | 65 | it('should register device', function () { 66 | return db.Device.register('Android', TEST_GCM_DEVICE); 67 | }); 68 | 69 | it('should save registration in cookie', function () { 70 | if (helper.isWebKit) { 71 | // TODO: we are currently using 3rd party cookies to store the device registration state 72 | // TODO: which is not supported by Webkit anymore 73 | return this.skip(); 74 | } 75 | 76 | var deviceId; 77 | return db.Device.register('Android', TEST_GCM_DEVICE).then(function (device) { 78 | deviceId = device.id; 79 | return new DB.EntityManagerFactory({ host: env.TEST_SERVER, staleness: 0 }).createEntityManager(true).ready(); 80 | }).then(function (newDB) { 81 | expect(newDB.isDeviceRegistered).be.true; 82 | expect(newDB.Device.isRegistered).be.true; 83 | expect(newDB.Device.me).be.ok; 84 | expect(newDB.Device.me.id).eql(deviceId); 85 | }); 86 | }); 87 | 88 | it('should push message', function () { 89 | return db.login('root', 'root').then(function () { 90 | return db.Device.register('Android', TEST_GCM_DEVICE); 91 | }).then(function () { 92 | return db.Device.find().equal('deviceOs', 'Android').resultList(); 93 | }).then(function (result) { 94 | expect(result).length.at.least(1); 95 | var msg = new db.Device.PushMessage(result, 'Message', 'Subject'); 96 | msg.sound = 'default'; 97 | msg.badge = 5; 98 | msg.data = { 99 | test: 'test', 100 | }; 101 | return db.Device.push(msg); 102 | }); 103 | }); 104 | 105 | it('should create correct json from push message', function () { 106 | return db.Device.register('Android', TEST_GCM_DEVICE) 107 | .then(function (device) { 108 | var msg1 = new db.Device.PushMessage(); 109 | msg1.addDevice(device); 110 | msg1.message = 'TestMSG'; 111 | msg1.subject = 'TestSubject'; 112 | msg1.badge = 5; 113 | msg1.data = { 114 | test: 'test', 115 | }; 116 | msg1.sound = 'test'; 117 | var msg2 = new db.Device.PushMessage(device, 'TestMSG', 'TestSubject', 'test', 5, { test: 'test' }); 118 | expect(msg2.toJSON()).eql(msg1.toJSON()); 119 | }); 120 | }); 121 | 122 | it('should not be allowed to insert device', async function () { 123 | var device = new db.Device(); 124 | try { 125 | await device.save(); 126 | expect.fail(); 127 | } catch (e) { 128 | expect(e.message).include('are not allowed'); 129 | } 130 | }); 131 | 132 | it('should remove cookie if device cannot be found', async function () { 133 | if (helper.isWebKit) { 134 | // TODO: we are currently using 3rd party cookies to store the device registration state 135 | // TODO: which is not supported by Webkit anymore 136 | return this.skip(); 137 | } 138 | 139 | await db.Device.register('Android', TEST_GCM_DEVICE); 140 | const newDB= await new DB.EntityManagerFactory({ host: env.TEST_SERVER, staleness: 0 }) 141 | .createEntityManager(true) 142 | .ready(); 143 | expect(newDB.isDeviceRegistered).be.true; 144 | expect(newDB.Device.isRegistered).be.true; 145 | expect(newDB.Device.me).be.ok; 146 | 147 | await newDB.Device.me.delete({ force: true }); 148 | DB.connector.Connector.connections = {}; 149 | const newDB2 = await new DB.EntityManagerFactory({ host: env.TEST_SERVER, staleness: 0 }) 150 | .createEntityManager(true) 151 | .ready(); 152 | 153 | expect(newDB2.isDeviceRegistered).be.false; 154 | expect(newDB2.Device.isRegistered).be.false; 155 | expect(newDB2.Device.me).be.null; 156 | }); 157 | 158 | if (typeof ArrayBuffer === 'undefined') { 159 | return; 160 | } 161 | 162 | it('should provide the WebPush key as an ArrayBuffer array', function () { 163 | return db.Device.loadWebPushKey().then(function (webPushKey) { 164 | expect(webPushKey).be.ok; 165 | expect(webPushKey).instanceOf(ArrayBuffer); 166 | }); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /lib/metamodel/ModelBuilder.ts: -------------------------------------------------------------------------------- 1 | import { BasicType } from './BasicType'; 2 | import { EntityType } from './EntityType'; 3 | import { EmbeddableType } from './EmbeddableType'; 4 | import { ListAttribute } from './ListAttribute'; 5 | import { MapAttribute } from './MapAttribute'; 6 | import { SetAttribute } from './SetAttribute'; 7 | import { SingularAttribute } from './SingularAttribute'; 8 | import { PersistentError } from '../error'; 9 | import { JsonMap } from '../util'; 10 | import { Type } from './Type'; 11 | import { ManagedType } from './ManagedType'; 12 | import { Attribute } from './Attribute'; 13 | import { Permission, Validator } from '../intersection'; 14 | 15 | export class ModelBuilder { 16 | private models: { [name: string]: Type } = {}; 17 | 18 | private modelDescriptors: { [name: string]: JsonMap } | null = null; 19 | 20 | constructor() { 21 | (Object.keys(BasicType) as (keyof typeof BasicType)[]).forEach((typeName) => { 22 | const basicType = BasicType[typeName]; 23 | if (basicType instanceof BasicType) { 24 | this.models[basicType.ref] = basicType; 25 | } 26 | }); 27 | } 28 | 29 | /** 30 | * @param ref 31 | * @return 32 | */ 33 | getModel(ref: string): ManagedType { 34 | if (ref in this.models) { 35 | return this.models[ref] as ManagedType; 36 | } 37 | 38 | const model = this.buildModel(ref); 39 | this.models[ref] = model; 40 | return model; 41 | } 42 | 43 | /** 44 | * @param modelDescriptors 45 | * @return 46 | */ 47 | buildModels(modelDescriptors: JsonMap[]): { [name: string]: Type } { 48 | this.modelDescriptors = {}; 49 | 50 | modelDescriptors.forEach((modelDescriptor: JsonMap) => { 51 | this.modelDescriptors![modelDescriptor.class as string] = modelDescriptor; 52 | }); 53 | 54 | Object.keys(this.modelDescriptors).forEach((ref) => { 55 | try { 56 | const model = this.getModel(ref); 57 | this.buildAttributes(model); 58 | } catch (e: any) { 59 | throw new PersistentError(`Can't create model for entity class ${ref}`, e); 60 | } 61 | }); 62 | 63 | // ensure at least an object entity 64 | this.getModel(EntityType.Object.ref); 65 | 66 | return this.models; 67 | } 68 | 69 | /** 70 | * @param ref 71 | * @return 72 | */ 73 | buildModel(ref: string): ManagedType { 74 | const modelDescriptor = this.modelDescriptors![ref]; 75 | let type: ManagedType; 76 | if (ref === EntityType.Object.ref) { 77 | type = new EntityType.Object(); 78 | } else if (modelDescriptor) { 79 | if (modelDescriptor.embedded) { 80 | type = new EmbeddableType(ref); 81 | } else { 82 | const superTypeIdentifier = modelDescriptor.superClass as string || EntityType.Object.ref; 83 | type = new EntityType(ref, this.getModel(superTypeIdentifier) as EntityType); 84 | } 85 | } else { 86 | throw new TypeError(`No model available for ${ref}`); 87 | } 88 | 89 | type.metadata = {}; 90 | 91 | if (modelDescriptor) { 92 | type.metadata = modelDescriptor.metadata as { [key: string]: string } || {}; 93 | const permissions = modelDescriptor.acl || {}; 94 | (Object.keys(permissions) as Array).forEach((permission) => { 95 | const permissionProperty = `${permission}Permission`; 96 | ((type as any)[permissionProperty] as Permission).fromJSON(permissions[permission]); 97 | }); 98 | } 99 | 100 | return type; 101 | } 102 | 103 | /** 104 | * @param model 105 | * @return 106 | */ 107 | buildAttributes(model: ManagedType): void { 108 | const modelDescriptor = this.modelDescriptors![model.ref]; 109 | const fields = modelDescriptor.fields as JsonMap; 110 | 111 | Object.keys(fields).forEach((name) => { 112 | const field = fields[name] as JsonMap; 113 | if (!model.getAttribute(name)) { // skip predefined attributes 114 | model.addAttribute(this.buildAttribute(field as any), field.order as number); 115 | } 116 | }); 117 | 118 | if (typeof modelDescriptor.validationCode === 'string') { 119 | // eslint-disable-next-line no-param-reassign 120 | (model as EntityType).validationCode = Validator.compile(model, modelDescriptor.validationCode); 121 | } 122 | } 123 | 124 | /** 125 | * @param field The field metadata 126 | * @param field.name The name of zhe field 127 | * @param field.type The type reference of the field 128 | * @param field.order The order number of the field 129 | * @param field.metadata Additional metadata of the field 130 | * @return 131 | */ 132 | buildAttribute(field: { name: string, type: string, order: number, metadata: { [key: string]: string }, 133 | flags: string[] }): Attribute { 134 | // TODO: remove readonly if createdAt and updatedAt becomes real metadata fields in the schema 135 | const isMetadata = field.flags && (field.flags.indexOf('METADATA') !== -1 || field.flags.indexOf('READONLY') !== -1); 136 | const { name } = field; 137 | const ref = field.type; 138 | if (ref.indexOf('/db/collection.') !== 0) { 139 | const singularAttribute = new SingularAttribute(name, this.getModel(ref), isMetadata); 140 | singularAttribute.metadata = field.metadata; 141 | return singularAttribute; 142 | } 143 | const collectionType = ref.substring(0, ref.indexOf('[')); 144 | const elementType = ref.substring(ref.indexOf('[') + 1, ref.indexOf(']')).trim(); 145 | 146 | switch (collectionType) { 147 | case ListAttribute.ref: 148 | return new ListAttribute(name, this.getModel(elementType)); 149 | case SetAttribute.ref: 150 | return new SetAttribute(name, this.getModel(elementType)); 151 | case MapAttribute.ref: { 152 | const keyType = elementType.substring(0, elementType.indexOf(',')).trim(); 153 | const valueType = elementType.substring(elementType.indexOf(',') + 1).trim(); 154 | 155 | return new MapAttribute(name, this.getModel(keyType), this.getModel(valueType)); 156 | } 157 | default: 158 | throw new TypeError(`No collection available for ${ref}`); 159 | } 160 | } 161 | } 162 | --------------------------------------------------------------------------------