├── examples ├── setup.ts ├── crm │ ├── entryTypes │ │ ├── product.ts │ │ └── customerAccount.ts │ ├── main.ts │ └── crmExtension.ts └── basic │ └── main.ts ├── src ├── serve │ ├── utils.ts │ ├── cors.ts │ ├── exeption │ │ └── cloud-exception.ts │ ├── middleware.ts │ ├── cloud-server.ts │ ├── request-lifecycle.ts │ ├── in-context.ts │ ├── server-exception.ts │ ├── path-handler.ts │ └── request-handler.ts ├── orm │ ├── db │ │ └── postgres │ │ │ ├── in-pg │ │ │ ├── src │ │ │ │ ├── inpg.data │ │ │ │ ├── inpg.wasm │ │ │ │ ├── utils.ts │ │ │ │ └── constants.ts │ │ │ ├── types.ts │ │ │ └── cloud-db.ts │ │ │ ├── pgAuth.ts │ │ │ ├── pgUtils.ts │ │ │ ├── README.md │ │ │ ├── maps │ │ │ └── maps.ts │ │ │ └── pgError.ts │ ├── settings │ │ ├── settings-base.ts │ │ ├── build-settings.ts │ │ └── types.ts │ ├── mod.ts │ ├── roles │ │ ├── settings-permissions.ts │ │ ├── entry-permissions.ts │ │ └── shared-permissions.ts │ ├── setup │ │ ├── entry-type │ │ │ ├── validate-entry-type.ts │ │ │ └── build-entry-types.ts │ │ └── settings-type │ │ │ ├── validate-settings-type.ts │ │ │ └── build-settings-types.ts │ ├── field │ │ ├── fields │ │ │ ├── url-field.ts │ │ │ ├── date-field.ts │ │ │ ├── email-field.ts │ │ │ ├── list-field.ts │ │ │ ├── phone-field.ts │ │ │ ├── text-field.ts │ │ │ ├── big-int-field.ts │ │ │ ├── choices-field.ts │ │ │ ├── int-field.ts │ │ │ ├── password-field.ts │ │ │ ├── decimal-field.ts │ │ │ ├── rich-text-field.ts │ │ │ ├── multi-choice-field.ts │ │ │ ├── time-field.ts │ │ │ ├── file-field.ts │ │ │ ├── image-field.ts │ │ │ ├── currency-field.ts │ │ │ ├── data-field.ts │ │ │ ├── timestamp-field.ts │ │ │ ├── json-field.ts │ │ │ ├── boolean-field.ts │ │ │ ├── connection-field.ts │ │ │ └── id-field.ts │ │ ├── types.ts │ │ └── fields.ts │ ├── build │ │ ├── generate-interface │ │ │ ├── files-handling.ts │ │ │ ├── field-type-map.ts │ │ │ ├── build-fields.ts │ │ │ └── generate-client-interface.ts │ │ └── make-fields.ts │ ├── entry │ │ ├── entry-base.ts │ │ └── build-entry.ts │ ├── migrate │ │ ├── settings-type │ │ │ └── settings-migration-plan.ts │ │ ├── types.ts │ │ ├── cloud-migrator.ts │ │ ├── migration-plan.ts │ │ ├── entry-type │ │ │ └── entry-migration-plan.ts │ │ └── migrate-utils.ts │ ├── utils │ │ └── ulid.ts │ ├── orm-exception.ts │ ├── api-actions │ │ ├── groups.ts │ │ └── orm-group.ts │ ├── orm-types.ts │ ├── shared │ │ └── shared-types.ts │ ├── child-entry │ │ └── build-children.ts │ └── registry │ │ └── connection-registry.ts ├── auth │ ├── entries │ │ ├── account │ │ │ ├── onboarding.ts │ │ │ ├── account.ts │ │ │ └── _account.type.ts │ │ ├── user │ │ │ ├── actions │ │ │ │ ├── generate-api-token.ts │ │ │ │ ├── generate-reset-token.ts │ │ │ │ ├── find-accounts.ts │ │ │ │ ├── set-password.ts │ │ │ │ └── validate-password.ts │ │ │ ├── fields │ │ │ │ ├── google-fields.ts │ │ │ │ └── fields.ts │ │ │ └── user-entry.ts │ │ └── user-session │ │ │ ├── fields.ts │ │ │ ├── user-session-entry.ts │ │ │ └── _user-session.type.ts │ ├── types.ts │ ├── actions │ │ ├── auth-check.ts │ │ ├── get-account.ts │ │ ├── logout.ts │ │ ├── set-new-password.ts │ │ ├── update-account.ts │ │ ├── login.ts │ │ ├── register-user.ts │ │ ├── reset-password.ts │ │ ├── google │ │ │ ├── google-token-login.ts │ │ │ ├── signup-google.ts │ │ │ ├── login-google.ts │ │ │ └── handle-google-login.ts │ │ └── register-account.ts │ ├── settings │ │ ├── auth-settings.ts │ │ ├── _auth-settings.type.ts │ │ └── field-groups │ │ │ └── google-fields.ts │ ├── security.ts │ ├── auth-lifecycle.ts │ ├── auth-middleware.ts │ ├── auth-group.ts │ └── migrate │ │ └── init-admin-account.ts ├── types │ ├── mod.ts │ └── serve-types.ts ├── in-queue │ ├── types.ts │ ├── entry-types │ │ └── in-task │ │ │ ├── fields.ts │ │ │ └── _in-task-global.type.ts │ └── in-queue-client.ts ├── utils │ ├── file-handling.ts │ ├── mod.ts │ ├── misc.ts │ └── path-utils.ts ├── in-live │ ├── in-live-room.ts │ ├── in-live-middleware.ts │ ├── in-live-broker.ts │ ├── types.ts │ └── broker-client.ts ├── onboarding │ ├── _onboarding.type.ts │ ├── ob-settings.ts │ └── actions │ │ └── complete-onboarding.ts ├── files │ ├── actions │ │ ├── files-group.ts │ │ ├── get-file.ts │ │ └── upload-file.ts │ ├── mime-types │ │ ├── mime-types.ts │ │ ├── generate.ts │ │ └── file-types.ts │ └── entries │ │ └── cloud-file.ts ├── email │ ├── actions │ │ ├── email-group.ts │ │ └── sendEmail.ts │ ├── email-manager.ts │ ├── settings │ │ ├── _email-settings.type.ts │ │ └── emailSettings.ts │ └── smtp │ │ └── smtpTypes.ts ├── extension │ ├── settings │ │ ├── _system-settings.type.ts │ │ └── systemSettings.ts │ ├── action-groups │ │ └── dev-actions.ts │ ├── core-config.ts │ └── orm-hooks │ │ └── in-live-notify.ts ├── static │ └── staticPathHandler.ts ├── terminal │ ├── types.ts │ ├── terminal.ts │ └── box.ts ├── api │ ├── api-handler.ts │ ├── api-types.ts │ └── cloud-group.ts ├── cloud-config │ └── config-types.ts └── in-log │ └── types.ts ├── .vscode ├── settings.json └── extensions.json ├── extensions ├── mod.ts ├── flutter │ └── mod.ts └── user-agent │ ├── mod.ts │ └── src │ └── extension.ts ├── .gitignore ├── .gitmodules ├── cli └── src │ ├── config-types.ts │ ├── types.ts │ ├── utils.ts │ ├── multicore.ts │ └── cloud-config.ts ├── .zed ├── tasks.json └── settings.json ├── .github └── workflows │ └── publish.yaml ├── mod.ts ├── deno.json └── types.ts /examples/setup.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/serve/utils.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "denoland.vscode-deno" 4 | ] 5 | } -------------------------------------------------------------------------------- /extensions/mod.ts: -------------------------------------------------------------------------------- 1 | import flutterExtension from "#extensions/flutter/mod.ts"; 2 | export { flutterExtension }; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .inspatial/ 2 | deno.lock 3 | cloud-config.json 4 | pgdata/ 5 | debug.log 6 | .zed/ 7 | .vscode/ 8 | public/ 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "cloud-client"] 2 | path = cloud-client 3 | url = https://github.com/inspatiallabs/cloud-client.git 4 | -------------------------------------------------------------------------------- /src/orm/db/postgres/in-pg/src/inpg.data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReimuNotMoe/inspatial-cloud/main/src/orm/db/postgres/in-pg/src/inpg.data -------------------------------------------------------------------------------- /src/orm/db/postgres/in-pg/src/inpg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReimuNotMoe/inspatial-cloud/main/src/orm/db/postgres/in-pg/src/inpg.wasm -------------------------------------------------------------------------------- /cli/src/config-types.ts: -------------------------------------------------------------------------------- 1 | import type { CoreConfig } from "~/extension/core-config.ts"; 2 | 3 | export interface BuiltInConfig { 4 | core: CoreConfig; 5 | } 6 | -------------------------------------------------------------------------------- /examples/crm/entryTypes/product.ts: -------------------------------------------------------------------------------- 1 | import { EntryType } from "@inspatial/cloud"; 2 | 3 | export const product = new EntryType("product", { 4 | fields: [], 5 | }); 6 | -------------------------------------------------------------------------------- /cli/src/types.ts: -------------------------------------------------------------------------------- 1 | export type CloudRunnerMode = 2 | | "init" 3 | | "server" 4 | | "migrator" 5 | | "broker" 6 | | "queue" 7 | | "db"; 8 | export type RunnerMode = CloudRunnerMode; 9 | -------------------------------------------------------------------------------- /src/auth/entries/account/onboarding.ts: -------------------------------------------------------------------------------- 1 | import { ChildEntryType } from "@inspatial/cloud"; 2 | 3 | export const accountOnboardingStep = new ChildEntryType("onboarding", { 4 | fields: [], 5 | }); 6 | -------------------------------------------------------------------------------- /src/types/mod.ts: -------------------------------------------------------------------------------- 1 | import type { CloudExtension } from "~/extension/cloud-extension.ts"; 2 | 3 | export type AppMode = "development" | "production"; 4 | 5 | export interface CloudConfig { 6 | extensions?: Array; 7 | } 8 | -------------------------------------------------------------------------------- /src/orm/settings/settings-base.ts: -------------------------------------------------------------------------------- 1 | import type { Settings } from "~/orm/settings/settings.ts"; 2 | 3 | export interface SettingsBase extends Settings { 4 | } 5 | 6 | export interface GenericSettings extends SettingsBase { 7 | [key: string]: any; 8 | } 9 | -------------------------------------------------------------------------------- /src/in-queue/types.ts: -------------------------------------------------------------------------------- 1 | export type ConnectionStatus = 2 | | "CONNECTING" 3 | | "OPEN" 4 | | "CLOSING" 5 | | "CLOSED" 6 | | "UNKNOWN"; 7 | 8 | export interface TaskInfo { 9 | id: string; 10 | systemGlobal: boolean; 11 | account?: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/orm/mod.ts: -------------------------------------------------------------------------------- 1 | export { raiseORMException } from "./orm-exception.ts"; 2 | 3 | export { InSpatialORM } from "~/orm/inspatial-orm.ts"; 4 | 5 | export { SettingsType } from "~/orm/settings/settings-type.ts"; 6 | 7 | export { EntryType } from "~/orm/entry/entry-type.ts"; 8 | -------------------------------------------------------------------------------- /src/orm/db/postgres/pgAuth.ts: -------------------------------------------------------------------------------- 1 | export const AUTH = { 2 | CLEAR_TEXT: 3, 3 | GSS_CONTINUE: 8, 4 | GSS_STARTUP: 7, 5 | MD5: 5, 6 | NO_AUTHENTICATION: 0, 7 | SASL_CONTINUE: 11, 8 | SASL_FINAL: 12, 9 | SASL_STARTUP: 10, 10 | SCM: 6, 11 | SSPI: 9, 12 | } as const; 13 | -------------------------------------------------------------------------------- /src/utils/file-handling.ts: -------------------------------------------------------------------------------- 1 | export function hasDirectory(path: string): boolean { 2 | try { 3 | const stat = Deno.statSync(path); 4 | return stat.isDirectory; 5 | } catch (e) { 6 | if (e instanceof Deno.errors.NotFound) { 7 | return false; 8 | } 9 | throw e; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/in-live/in-live-room.ts: -------------------------------------------------------------------------------- 1 | import type { InLiveUser } from "~/in-live/types.ts"; 2 | 3 | export class InLiveRoom { 4 | roomName: string; 5 | 6 | clients: Set = new Set(); 7 | users: Map = new Map(); 8 | constructor(roomName: string) { 9 | this.roomName = roomName; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.zed/tasks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "command": "deno", 4 | "args": ["run", "-A", "main.ts"], 5 | "cwd": "$ZED_WORKTREE_ROOT/examples/basic", 6 | "label": "Example: Basic", 7 | "allow_concurrent_runs": false, 8 | "reveal": "always", 9 | "use_new_terminal": false, 10 | "reveal_target": "dock" 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /src/utils/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "~/utils/convert-string.ts"; 2 | 3 | export * from "~/terminal/format-utils.ts"; 4 | 5 | export * from "~/terminal/color-me.ts"; 6 | 7 | export * from "~/terminal/print-utils.ts"; 8 | 9 | export * from "~/utils/date-utils.ts"; 10 | 11 | export * from "~/utils/misc.ts"; 12 | 13 | export * from "~/utils/path-utils.ts"; 14 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | 11 | permissions: 12 | contents: read 13 | id-token: write 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Publish package 19 | run: npx jsr publish -------------------------------------------------------------------------------- /src/auth/types.ts: -------------------------------------------------------------------------------- 1 | export interface SessionData extends UserContext { 2 | email: string; 3 | firstName: string; 4 | lastName: string; 5 | systemAdmin: boolean; 6 | [key: string]: any; 7 | } 8 | 9 | export interface UserContext extends UserID { 10 | accountId: string; 11 | } 12 | 13 | export interface UserID { 14 | userId: string; 15 | role: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/onboarding/_onboarding.type.ts: -------------------------------------------------------------------------------- 1 | import type { SettingsBase } from "@inspatial/cloud/types"; 2 | 3 | export interface Onboarding extends SettingsBase { 4 | _name: "onboarding"; 5 | /** 6 | * **Enable Onboarding** (BooleanField) 7 | * @description Enable or disable onboarding for new users 8 | * @type {boolean} 9 | */ 10 | enabled?: boolean; 11 | } 12 | -------------------------------------------------------------------------------- /src/auth/actions/auth-check.ts: -------------------------------------------------------------------------------- 1 | import { CloudAPIAction } from "~/api/cloud-action.ts"; 2 | 3 | const authCheck = new CloudAPIAction("authCheck", { 4 | description: "Check if user is authenticated", 5 | run({ inRequest }) { 6 | const user = inRequest.context.get("user"); 7 | 8 | return user; 9 | }, 10 | params: [], 11 | }); 12 | 13 | export default authCheck; 14 | -------------------------------------------------------------------------------- /src/orm/roles/settings-permissions.ts: -------------------------------------------------------------------------------- 1 | import type { BasePermission } from "./shared-permissions.ts"; 2 | 3 | export interface SettingsRole { 4 | roleName: string; 5 | permission: SettingsPermission; 6 | } 7 | 8 | export interface SettingsPermission 9 | extends BasePermission { 10 | } 11 | -------------------------------------------------------------------------------- /src/files/actions/files-group.ts: -------------------------------------------------------------------------------- 1 | import { CloudAPIGroup } from "~/api/cloud-group.ts"; 2 | import { getFile } from "./get-file.ts"; 3 | import { uploadFile } from "./upload-file.ts"; 4 | 5 | const filesGroup = new CloudAPIGroup("files", { 6 | label: "Files", 7 | description: "File Management", 8 | actions: [getFile, uploadFile], 9 | }); 10 | 11 | export default filesGroup; 12 | -------------------------------------------------------------------------------- /src/email/actions/email-group.ts: -------------------------------------------------------------------------------- 1 | import { sendEmail } from "./sendEmail.ts"; 2 | import { redirectAction } from "./googleRedirect.ts"; 3 | import { CloudAPIGroup } from "~/api/cloud-group.ts"; 4 | 5 | export const emailGroup: CloudAPIGroup<"email"> = new CloudAPIGroup("email", { 6 | label: "Email", 7 | description: "Email management", 8 | actions: [sendEmail, redirectAction], 9 | }); 10 | -------------------------------------------------------------------------------- /src/onboarding/ob-settings.ts: -------------------------------------------------------------------------------- 1 | import { SettingsType } from "~/orm/settings/settings-type.ts"; 2 | 3 | export const onboardingSettings = new SettingsType("onboarding", { 4 | fields: [{ 5 | key: "enabled", 6 | label: "Enable Onboarding", 7 | type: "BooleanField", 8 | defaultValue: true, 9 | description: "Enable or disable onboarding for new users", 10 | }], 11 | }); 12 | -------------------------------------------------------------------------------- /src/orm/setup/entry-type/validate-entry-type.ts: -------------------------------------------------------------------------------- 1 | import type { EntryType } from "~/orm/entry/entry-type.ts"; 2 | import { validateConnectionFields } from "~/orm/setup/setup-utils.ts"; 3 | import type { Role } from "../../roles/role.ts"; 4 | 5 | export function validateEntryType( 6 | role: Role, 7 | entryType: EntryType, 8 | ): void { 9 | validateConnectionFields(role, entryType); 10 | } 11 | -------------------------------------------------------------------------------- /src/orm/setup/settings-type/validate-settings-type.ts: -------------------------------------------------------------------------------- 1 | import type { Role } from "../../roles/role.ts"; 2 | import type { SettingsType } from "../../settings/settings-type.ts"; 3 | import { validateConnectionFields } from "../setup-utils.ts"; 4 | 5 | export function validateSettingsType( 6 | role: Role, 7 | settingsType: SettingsType, 8 | ): void { 9 | validateConnectionFields(role, settingsType); 10 | } 11 | -------------------------------------------------------------------------------- /src/in-live/in-live-middleware.ts: -------------------------------------------------------------------------------- 1 | import type { Middleware } from "~/serve/middleware.ts"; 2 | 3 | export const inLiveMiddleware: Middleware = { 4 | name: "Realtime Middleware", 5 | description: "Realtime Middleware for InSpatialServer", 6 | handler(app, inRequest) { 7 | if (inRequest.upgradeSocket && inRequest.path === "/ws") { 8 | return app.inLive.handleUpgrade(inRequest); 9 | } 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/auth/settings/auth-settings.ts: -------------------------------------------------------------------------------- 1 | import { SettingsType } from "~/orm/settings/settings-type.ts"; 2 | import { googleFields } from "~/auth/settings/field-groups/google-fields.ts"; 3 | import type { AuthSettings } from "./_auth-settings.type.ts"; 4 | 5 | export const authSettings = new SettingsType("authSettings", { 6 | label: "Auth Settings", 7 | systemGlobal: true, 8 | fields: [...googleFields], 9 | }); 10 | -------------------------------------------------------------------------------- /src/extension/settings/_system-settings.type.ts: -------------------------------------------------------------------------------- 1 | import type { SettingsBase } from "@inspatial/cloud/types"; 2 | 3 | export interface SystemSettings extends SettingsBase { 4 | _name: "systemSettings"; 5 | /** 6 | * **Enable User Signup** (BooleanField) 7 | * @description Enable user signup for new accounts. Turn off to prevent new users from signing up. 8 | * @type {boolean} 9 | */ 10 | enableSignup?: boolean; 11 | } 12 | -------------------------------------------------------------------------------- /examples/basic/main.ts: -------------------------------------------------------------------------------- 1 | import { createInCloud } from "@inspatial/cloud"; 2 | import { EntryType } from "~/orm/mod.ts"; 3 | 4 | createInCloud({ 5 | name: "myAwesomeCloud", 6 | entryTypes: [ 7 | new EntryType("thing", { 8 | fields: [], 9 | actions: [{ 10 | key: "yo", 11 | params: [], 12 | action() { 13 | console.log("Hello from the cloud!"); 14 | }, 15 | }], 16 | }), 17 | ], 18 | }); 19 | -------------------------------------------------------------------------------- /src/extension/action-groups/dev-actions.ts: -------------------------------------------------------------------------------- 1 | import { CloudAPIGroup } from "@inspatial/cloud"; 2 | 3 | export const devActions = new CloudAPIGroup("dev", { 4 | description: "Development actions", 5 | actions: [], 6 | label: "Dev", 7 | }); 8 | 9 | devActions.addAction("generateConfig", { 10 | description: "Generate the cloud-config.json along with the schema", 11 | params: [], 12 | run({ inCloud }) { 13 | inCloud.generateConfigFile(); 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /src/orm/roles/entry-permissions.ts: -------------------------------------------------------------------------------- 1 | import type { BasePermission } from "~/orm/roles/shared-permissions.ts"; 2 | 3 | export interface EntryRole { 4 | roleName: string; 5 | permission: EntryPermission; 6 | } 7 | 8 | export interface EntryPermission 9 | extends BasePermission { 10 | create: boolean; 11 | delete: boolean; 12 | userScoped?: { 13 | userIdField: string; 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/orm/field/fields/url-field.ts: -------------------------------------------------------------------------------- 1 | import { ORMFieldConfig } from "~/orm/field/orm-field.ts"; 2 | 3 | export default new ORMFieldConfig("URLField", { 4 | dbColumn: (fieldDef) => { 5 | return { 6 | columnName: fieldDef.key, 7 | dataType: "text", 8 | }; 9 | }, 10 | dbLoad(value, _fieldDef) { 11 | return value; 12 | }, 13 | validate(_value, _fieldDef) { 14 | return true; 15 | }, 16 | dbSave(value, _fieldDef) { 17 | return value; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /src/orm/field/fields/date-field.ts: -------------------------------------------------------------------------------- 1 | import { ORMFieldConfig } from "~/orm/field/orm-field.ts"; 2 | 3 | export default new ORMFieldConfig("DateField", { 4 | dbColumn: (fieldDef) => { 5 | return { 6 | columnName: fieldDef.key, 7 | dataType: "date", 8 | }; 9 | }, 10 | dbLoad(value, _fieldDef) { 11 | return value; 12 | }, 13 | validate(_value, _fieldDef) { 14 | return true; 15 | }, 16 | dbSave(value, _fieldDef) { 17 | return value; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /src/orm/field/fields/email-field.ts: -------------------------------------------------------------------------------- 1 | import { ORMFieldConfig } from "~/orm/field/orm-field.ts"; 2 | 3 | export default new ORMFieldConfig("EmailField", { 4 | dbColumn: (fieldDef) => { 5 | return { 6 | columnName: fieldDef.key, 7 | dataType: "text", 8 | }; 9 | }, 10 | dbLoad(value, _fieldDef) { 11 | return value; 12 | }, 13 | validate(_value, _fieldDef) { 14 | return true; 15 | }, 16 | dbSave(value, _fieldDef) { 17 | return value; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /src/orm/field/fields/list-field.ts: -------------------------------------------------------------------------------- 1 | import { ORMFieldConfig } from "~/orm/field/orm-field.ts"; 2 | 3 | export default new ORMFieldConfig("ListField", { 4 | dbColumn: (fieldDef) => { 5 | return { 6 | columnName: fieldDef.key, 7 | dataType: "jsonb", 8 | }; 9 | }, 10 | dbLoad(value, _fieldDef) { 11 | return value; 12 | }, 13 | validate(_value, _fieldDef) { 14 | return true; 15 | }, 16 | dbSave(value, _fieldDef) { 17 | return value; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /src/orm/field/fields/phone-field.ts: -------------------------------------------------------------------------------- 1 | import { ORMFieldConfig } from "~/orm/field/orm-field.ts"; 2 | 3 | export default new ORMFieldConfig("PhoneField", { 4 | dbColumn: (fieldDef) => { 5 | return { 6 | columnName: fieldDef.key, 7 | dataType: "text", 8 | }; 9 | }, 10 | dbLoad(value, _fieldDef) { 11 | return value; 12 | }, 13 | validate(_value, _fieldDef) { 14 | return true; 15 | }, 16 | dbSave(value, _fieldDef) { 17 | return value; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /src/orm/field/fields/text-field.ts: -------------------------------------------------------------------------------- 1 | import { ORMFieldConfig } from "~/orm/field/orm-field.ts"; 2 | 3 | export default new ORMFieldConfig("TextField", { 4 | dbColumn: (fieldDef) => { 5 | return { 6 | columnName: fieldDef.key, 7 | dataType: "text", 8 | }; 9 | }, 10 | dbLoad(value, _fieldDef) { 11 | return value; 12 | }, 13 | validate(_value, _fieldDef) { 14 | return true; 15 | }, 16 | dbSave(value, _fieldDef) { 17 | return value; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /src/orm/field/fields/big-int-field.ts: -------------------------------------------------------------------------------- 1 | import { ORMFieldConfig } from "~/orm/field/orm-field.ts"; 2 | 3 | export default new ORMFieldConfig("BigIntField", { 4 | dbColumn: (fieldDef) => { 5 | return { 6 | columnName: fieldDef.key, 7 | dataType: "bigint", 8 | }; 9 | }, 10 | dbLoad(value, _fieldDef) { 11 | return value; 12 | }, 13 | validate(_value, _fieldDef) { 14 | return true; 15 | }, 16 | dbSave(value, _fieldDef) { 17 | return value; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /src/orm/field/fields/choices-field.ts: -------------------------------------------------------------------------------- 1 | import { ORMFieldConfig } from "~/orm/field/orm-field.ts"; 2 | 3 | export default new ORMFieldConfig("ChoicesField", { 4 | dbColumn: (fieldDef) => { 5 | return { 6 | columnName: fieldDef.key, 7 | dataType: "text", 8 | }; 9 | }, 10 | dbLoad(value, _fieldDef) { 11 | return value; 12 | }, 13 | validate(_value, _fieldDef) { 14 | return true; 15 | }, 16 | dbSave(value, _fieldDef) { 17 | return value; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /src/orm/field/fields/int-field.ts: -------------------------------------------------------------------------------- 1 | import { ORMFieldConfig } from "~/orm/field/orm-field.ts"; 2 | /** */ 3 | export default new ORMFieldConfig("IntField", { 4 | dbColumn: (fieldDef) => { 5 | return { 6 | columnName: fieldDef.key, 7 | dataType: "integer", 8 | }; 9 | }, 10 | dbLoad(value, _fieldDef) { 11 | return value; 12 | }, 13 | validate(_value, _fieldDef) { 14 | return true; 15 | }, 16 | dbSave(value, _fieldDef) { 17 | return value; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /src/orm/field/fields/password-field.ts: -------------------------------------------------------------------------------- 1 | import { ORMFieldConfig } from "~/orm/field/orm-field.ts"; 2 | 3 | export default new ORMFieldConfig("PasswordField", { 4 | dbColumn: (fieldDef) => { 5 | return { 6 | columnName: fieldDef.key, 7 | dataType: "text", 8 | }; 9 | }, 10 | dbLoad(value, _fieldDef) { 11 | return value; 12 | }, 13 | validate(_value, _fieldDef) { 14 | return true; 15 | }, 16 | dbSave(value, _fieldDef) { 17 | return value; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /src/orm/field/fields/decimal-field.ts: -------------------------------------------------------------------------------- 1 | import { ORMFieldConfig } from "~/orm/field/orm-field.ts"; 2 | 3 | export default new ORMFieldConfig("DecimalField", { 4 | dbColumn: (fieldDef) => { 5 | return { 6 | columnName: fieldDef.key, 7 | dataType: "numeric", 8 | }; 9 | }, 10 | dbLoad(value, _fieldDef) { 11 | return value; 12 | }, 13 | validate(_value, _fieldDef) { 14 | return true; 15 | }, 16 | dbSave(value, _fieldDef) { 17 | return value; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /src/orm/field/fields/rich-text-field.ts: -------------------------------------------------------------------------------- 1 | import { ORMFieldConfig } from "~/orm/field/orm-field.ts"; 2 | 3 | export default new ORMFieldConfig("RichTextField", { 4 | dbColumn: (fieldDef) => { 5 | return { 6 | columnName: fieldDef.key, 7 | dataType: "text", 8 | }; 9 | }, 10 | dbLoad(value, _fieldDef) { 11 | return value; 12 | }, 13 | validate(_value, _fieldDef) { 14 | return true; 15 | }, 16 | dbSave(value, _fieldDef) { 17 | return value; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /src/orm/field/fields/multi-choice-field.ts: -------------------------------------------------------------------------------- 1 | import { ORMFieldConfig } from "~/orm/field/orm-field.ts"; 2 | 3 | export default new ORMFieldConfig("MultiChoiceField", { 4 | dbColumn: (fieldDef) => { 5 | return { 6 | columnName: fieldDef.key, 7 | dataType: "text", 8 | }; 9 | }, 10 | dbLoad(value, _fieldDef) { 11 | return value; 12 | }, 13 | validate(_value, _fieldDef) { 14 | return true; 15 | }, 16 | dbSave(value, _fieldDef) { 17 | return value; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /src/static/staticPathHandler.ts: -------------------------------------------------------------------------------- 1 | import type { PathHandler } from "~/serve/path-handler.ts"; 2 | 3 | export const staticFilesHandler: PathHandler = { 4 | name: "staticFiles", 5 | description: "Static files handler", 6 | // Match all paths since this is the last handler and is a fallback 7 | // It will handle all requests that do not match any other handler 8 | match: /.*/, 9 | async handler(inCloud, inRequest, inResponse) { 10 | return await inCloud.static.serveFile(inRequest, inResponse); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/misc.ts: -------------------------------------------------------------------------------- 1 | export function generateId(length?: number): string { 2 | length = length || 16; 3 | const value = crypto.getRandomValues(new Uint8Array(length / 2)); 4 | return Array.from(value, (v) => v.toString(16).padStart(2, "0")).join(""); 5 | } 6 | 7 | export function isEmpty(value: unknown): boolean { 8 | return value === null || value === undefined || value === ""; 9 | } 10 | 11 | export function asyncPause(duration = 100) { 12 | return new Promise((resolve) => setTimeout(resolve, duration)); 13 | } 14 | -------------------------------------------------------------------------------- /src/orm/field/fields/time-field.ts: -------------------------------------------------------------------------------- 1 | import { ORMFieldConfig } from "~/orm/field/orm-field.ts"; 2 | 3 | export default new ORMFieldConfig("TimeField", { 4 | dbColumn: (fieldDef) => { 5 | return { 6 | columnName: fieldDef.key, 7 | dataType: "time", 8 | }; 9 | }, 10 | dbLoad(value, _fieldDef) { 11 | if (!value) return null; 12 | return value; 13 | }, 14 | validate(_value, _fieldDef) { 15 | return true; 16 | }, 17 | dbSave(value, _fieldDef) { 18 | return value; 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /src/serve/cors.ts: -------------------------------------------------------------------------------- 1 | import type { Middleware } from "~/serve/middleware.ts"; 2 | 3 | export const corsMiddleware: Middleware = { 4 | name: "CORS Middleware", 5 | description: "CORS Middleware for InSpatialServer", 6 | handler(app, inRequest, inResponse) { 7 | const origins = app.getExtensionConfigValue( 8 | "core", 9 | "allowedOrigins", 10 | ); 11 | 12 | if (origins?.has(inRequest.origin) || origins?.has("*")) { 13 | inResponse.setAllowOrigin(inRequest.origin); 14 | } 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /src/orm/build/generate-interface/files-handling.ts: -------------------------------------------------------------------------------- 1 | export async function formatInterfaceFile(filePath: string): Promise { 2 | const process = new Deno.Command(Deno.execPath(), { 3 | args: ["fmt", filePath], 4 | stdout: "piped", 5 | stderr: "piped", 6 | }).spawn(); 7 | const status = await process.status; 8 | return status.success; 9 | } 10 | export async function writeInterfaceFile( 11 | filePath: string, 12 | content: string, 13 | ): Promise { 14 | await Deno.writeTextFile(filePath, content); 15 | } 16 | -------------------------------------------------------------------------------- /src/orm/field/fields/file-field.ts: -------------------------------------------------------------------------------- 1 | import { ORMFieldConfig } from "~/orm/field/orm-field.ts"; 2 | 3 | export default new ORMFieldConfig("FileField", { 4 | dbColumn: (fieldDef) => { 5 | return { 6 | columnName: fieldDef.key, 7 | dataType: "character varying", 8 | characterMaximumLength: 26, 9 | }; 10 | }, 11 | dbLoad(value, _fieldDef) { 12 | return value; 13 | }, 14 | validate(_value, _fieldDef) { 15 | return true; 16 | }, 17 | dbSave(value, _fieldDef) { 18 | return value; 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /src/orm/field/fields/image-field.ts: -------------------------------------------------------------------------------- 1 | import { ORMFieldConfig } from "~/orm/field/orm-field.ts"; 2 | 3 | export default new ORMFieldConfig("ImageField", { 4 | dbColumn: (fieldDef) => { 5 | return { 6 | columnName: fieldDef.key, 7 | dataType: "character varying", 8 | characterMaximumLength: 26, 9 | }; 10 | }, 11 | dbLoad(value, _fieldDef) { 12 | return value; 13 | }, 14 | validate(_value, _fieldDef) { 15 | return true; 16 | }, 17 | dbSave(value, _fieldDef) { 18 | return value; 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /cli/src/utils.ts: -------------------------------------------------------------------------------- 1 | export async function allowPortBinding() { 2 | if (Deno.build.os !== "linux") { 3 | throw new Error("This function is only supported on Linux"); 4 | } 5 | 6 | const cmd = new Deno.Command("sudo", { 7 | args: ["setcap", "cap_net_bind_service=+ep", Deno.execPath()], 8 | stderr: "piped", 9 | }); 10 | const child = cmd.spawn(); 11 | const output = await child.output(); 12 | 13 | if (output.code !== 0) { 14 | throw new Error(new TextDecoder().decode(output.stderr)); 15 | } 16 | return output.code; 17 | } 18 | -------------------------------------------------------------------------------- /src/orm/setup/entry-type/build-entry-types.ts: -------------------------------------------------------------------------------- 1 | import type { EntryType } from "~/orm/entry/entry-type.ts"; 2 | import { buildConnectionFields } from "~/orm/setup/setup-utils.ts"; 3 | import type { Role } from "../../roles/role.ts"; 4 | 5 | export function buildEntryType( 6 | role: Role, 7 | entryType: EntryType, 8 | ): void { 9 | buildConnectionFields(role, entryType); 10 | if (!entryType.children) { 11 | return; 12 | } 13 | for (const child of entryType.children.values()) { 14 | buildConnectionFields(role, child); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/orm/field/fields/currency-field.ts: -------------------------------------------------------------------------------- 1 | import { ORMFieldConfig } from "~/orm/field/orm-field.ts"; 2 | 3 | export default new ORMFieldConfig("CurrencyField", { 4 | dbColumn: (fieldDef) => { 5 | return { 6 | columnName: fieldDef.key, 7 | dataType: "numeric", 8 | numericPrecision: 12, 9 | numericScale: 2, 10 | }; 11 | }, 12 | dbLoad(value, _fieldDef) { 13 | return value; 14 | }, 15 | validate(_value, _fieldDef) { 16 | return true; 17 | }, 18 | dbSave(value, _fieldDef) { 19 | return value; 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /src/orm/field/fields/data-field.ts: -------------------------------------------------------------------------------- 1 | import { ORMFieldConfig } from "~/orm/field/orm-field.ts"; 2 | 3 | export const dataField = new ORMFieldConfig("DataField", { 4 | dbColumn: (fieldDef) => { 5 | return { 6 | columnName: fieldDef.key, 7 | dataType: "character varying", 8 | characterMaximumLength: 255, 9 | }; 10 | }, 11 | dbLoad(value) { 12 | return value; 13 | }, 14 | validate(_value) { 15 | return true; 16 | }, 17 | dbSave(value) { 18 | return value; 19 | }, 20 | normalize(value) { 21 | return value; 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /src/orm/setup/settings-type/build-settings-types.ts: -------------------------------------------------------------------------------- 1 | import type { Role } from "../../roles/role.ts"; 2 | import type { SettingsType } from "~/orm/settings/settings-type.ts"; 3 | import { buildConnectionFields } from "~/orm/setup/setup-utils.ts"; 4 | 5 | export function buildSettingsType( 6 | role: Role, 7 | settingsType: SettingsType, 8 | ): void { 9 | buildConnectionFields(role, settingsType); 10 | if (!settingsType.children) { 11 | return; 12 | } 13 | for (const child of settingsType.children.values()) { 14 | buildConnectionFields(role, child); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/orm/roles/shared-permissions.ts: -------------------------------------------------------------------------------- 1 | export interface BasePermission { 2 | view: boolean; 3 | modify: boolean; 4 | fields?: FieldPermissions; 5 | actions?: ActionsPermissions; 6 | } 7 | 8 | type FieldPermissions = { 9 | [K in FK]?: FieldPermission; 10 | }; 11 | 12 | interface FieldPermission { 13 | view: boolean; 14 | modify: boolean; 15 | } 16 | 17 | interface ActionsPermissions { 18 | include?: string[]; 19 | exclude?: string[]; 20 | } 21 | 22 | type ActionPermission = "allowed" | "denied"; 23 | -------------------------------------------------------------------------------- /examples/crm/main.ts: -------------------------------------------------------------------------------- 1 | import { createInCloud } from "@inspatial/cloud"; 2 | 3 | import { product } from "./entryTypes/product.ts"; 4 | import { customerAccount } from "./entryTypes/customerAccount.ts"; 5 | 6 | createInCloud({ 7 | name: "CRM", 8 | description: "Customer Relationship Management", 9 | version: "1.0.0", 10 | entryTypes: [customerAccount, product], 11 | roles: [{ 12 | roleName: "customer", 13 | description: "A customer", 14 | label: "Customer", 15 | }, { 16 | roleName: "manager", 17 | description: "A manager", 18 | label: "Manager", 19 | }], 20 | }); 21 | -------------------------------------------------------------------------------- /src/extension/settings/systemSettings.ts: -------------------------------------------------------------------------------- 1 | import { SettingsType } from "~/orm/settings/settings-type.ts"; 2 | import type { SystemSettings } from "./_system-settings.type.ts"; 3 | 4 | export const systemSettings = new SettingsType( 5 | "systemSettings", 6 | { 7 | systemGlobal: true, 8 | fields: [{ 9 | key: "enableSignup", 10 | label: "Enable User Signup", 11 | type: "BooleanField", 12 | description: 13 | "Enable user signup for new accounts. Turn off to prevent new users from signing up.", 14 | defaultValue: true, 15 | }], 16 | }, 17 | ); 18 | -------------------------------------------------------------------------------- /src/auth/entries/user/actions/generate-api-token.ts: -------------------------------------------------------------------------------- 1 | import type { EntryActionDefinition } from "~/orm/entry/types.ts"; 2 | import type { User } from "../_user.type.ts"; 3 | import { generateSalt } from "../../../security.ts"; 4 | 5 | export const generateApiToken: EntryActionDefinition = { 6 | key: "generateApiToken", 7 | label: "Generate API Token", 8 | description: "Generate an API token for the user", 9 | async action({ user }): Promise<{ token: string }> { 10 | const token = generateSalt(); 11 | user.apiToken = token; 12 | 13 | await user.save(); 14 | return { token }; 15 | }, 16 | params: [], 17 | }; 18 | -------------------------------------------------------------------------------- /src/orm/field/fields/timestamp-field.ts: -------------------------------------------------------------------------------- 1 | import { ORMFieldConfig } from "~/orm/field/orm-field.ts"; 2 | 3 | export default new ORMFieldConfig("TimeStampField", { 4 | dbColumn: (fieldDef) => { 5 | return { 6 | columnName: fieldDef.key, 7 | dataType: "timestamp with time zone", 8 | }; 9 | }, 10 | dbLoad(value, _fieldDef) { 11 | return value; 12 | }, 13 | validate(_value, _fieldDef) { 14 | return true; 15 | }, 16 | dbSave(value, _fieldDef) { 17 | value = new Date(value).toUTCString(); 18 | if (value === "Invalid Date") { 19 | value = null; 20 | } 21 | return value; 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /extensions/flutter/mod.ts: -------------------------------------------------------------------------------- 1 | import { CloudExtension } from "~/extension/cloud-extension.ts"; 2 | import { CloudAPIGroup } from "~/api/cloud-group.ts"; 3 | import generateModels from "./actions/generate-models.ts"; 4 | 5 | const flutterExtension = new CloudExtension("flutter", { 6 | label: "Flutter Extension", 7 | description: "A Flutter extension for InSpatial", 8 | version: "0.0.1", 9 | actionGroups: [ 10 | new CloudAPIGroup("flutter", { 11 | description: "Flutter Actions", 12 | label: "Flutter", 13 | actions: [generateModels], 14 | }), 15 | ], 16 | }); 17 | 18 | export default flutterExtension as CloudExtension; 19 | -------------------------------------------------------------------------------- /src/auth/entries/user/actions/generate-reset-token.ts: -------------------------------------------------------------------------------- 1 | import type { EntryActionDefinition } from "~/orm/entry/types.ts"; 2 | import type { User } from "../_user.type.ts"; 3 | import { generateSalt } from "../../../security.ts"; 4 | 5 | export const generateResetToken: EntryActionDefinition = { 6 | key: "generateResetToken", 7 | label: "Generate Reset Token", 8 | description: "Generate a reset token for the user", 9 | async action({ user }): Promise<{ token: string }> { 10 | const token = generateSalt(); 11 | user.resetPasswordToken = token; 12 | await user.save(); 13 | return { token }; 14 | }, 15 | params: [], 16 | }; 17 | -------------------------------------------------------------------------------- /src/auth/entries/user-session/fields.ts: -------------------------------------------------------------------------------- 1 | import type { InField } from "~/orm/field/field-def-types.ts"; 2 | 3 | export default [{ 4 | key: "user", 5 | label: "User", 6 | type: "ConnectionField", 7 | entryType: "user", 8 | required: true, 9 | description: "The user associated with this session", 10 | }, { 11 | key: "sessionId", 12 | label: "Session ID", 13 | type: "DataField", 14 | description: "Unique identifier for the session", 15 | required: true, 16 | readOnly: true, 17 | }, { 18 | key: "sessionData", 19 | label: "Session Data", 20 | type: "JSONField", 21 | description: "Data associated with the session", 22 | }] as Array; 23 | -------------------------------------------------------------------------------- /src/orm/entry/entry-base.ts: -------------------------------------------------------------------------------- 1 | import type { Entry } from "~/orm/entry/entry.ts"; 2 | 3 | export interface EntryBase extends Entry { 4 | _name: string; 5 | createdAt: number; 6 | /** 7 | * **Updated At** (TimeStampField) 8 | * @description The date and time this entry was last updated 9 | * @type {number} 10 | * @required true 11 | */ 12 | updatedAt: number; 13 | /** 14 | * **First Name** (DataField) 15 | * @description The user's first name 16 | * @type {string} 17 | * @required true 18 | */ 19 | 20 | save(): Promise; 21 | } 22 | 23 | export interface GenericEntry extends EntryBase { 24 | [key: string]: any; 25 | } 26 | -------------------------------------------------------------------------------- /examples/crm/crmExtension.ts: -------------------------------------------------------------------------------- 1 | import { CloudExtension } from "@inspatial/cloud"; 2 | 3 | import { product } from "./entryTypes/product.ts"; 4 | import { customerAccount } from "./entryTypes/customerAccount.ts"; 5 | 6 | const crmExtension = new CloudExtension("crm", { 7 | label: "CRM", 8 | description: "Customer Relationship Management", 9 | version: "1.0.0", 10 | entryTypes: [customerAccount, product], 11 | roles: [{ 12 | roleName: "customer", 13 | description: "A customer", 14 | label: "Customer", 15 | }, { 16 | roleName: "manager", 17 | description: "A manager", 18 | label: "Manager", 19 | }], 20 | }); 21 | 22 | export default crmExtension; 23 | -------------------------------------------------------------------------------- /src/orm/field/fields/json-field.ts: -------------------------------------------------------------------------------- 1 | import { ORMFieldConfig } from "~/orm/field/orm-field.ts"; 2 | 3 | export default new ORMFieldConfig("JSONField", { 4 | dbColumn: (fieldDef) => { 5 | return { 6 | columnName: fieldDef.key, 7 | dataType: "jsonb", 8 | }; 9 | }, 10 | dbLoad(value, _fieldDef) { 11 | return value; 12 | }, 13 | validate(_value, _fieldDef) { 14 | return true; 15 | }, 16 | normalize(value, _fieldDef) { 17 | return value; 18 | }, 19 | dbSave(value, _fieldDef) { 20 | if (typeof value === "undefined" || value === null) { 21 | return null; // Handle undefined or null values 22 | } 23 | return JSON.stringify(value); // Ensure value is serializable 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | import MimeTypes from "~/files/mime-types/mime-types.ts"; 2 | export { 3 | CloudException, 4 | raiseCloudException, 5 | } from "~/serve/exeption/cloud-exception.ts"; 6 | export { 7 | raiseServerException, 8 | ServerException, 9 | } from "~/serve/server-exception.ts"; 10 | export { createInCloud } from "~/runner/in-cloud-runner.ts"; 11 | 12 | export { 13 | ChildEntry, 14 | ChildEntryList, 15 | ChildEntryType, 16 | } from "~/orm/child-entry/child-entry.ts"; 17 | 18 | export { CloudAPIAction } from "~/api/cloud-action.ts"; 19 | export { CloudAPIGroup } from "~/api/cloud-group.ts"; 20 | export { CloudExtension } from "~/extension/cloud-extension.ts"; 21 | 22 | export { MimeTypes }; 23 | 24 | export * from "~/orm/mod.ts"; 25 | -------------------------------------------------------------------------------- /src/auth/entries/user/actions/find-accounts.ts: -------------------------------------------------------------------------------- 1 | import type { EntryActionDefinition } from "~/orm/entry/types.ts"; 2 | import type { User } from "../_user.type.ts"; 3 | 4 | export const findAccounts: EntryActionDefinition = { 5 | key: "findAccounts", 6 | label: "Find Accounts", 7 | params: [], 8 | async action({ user, orm }) { 9 | const result = await orm.systemDb.getRows("childAccountUsers", { 10 | columns: ["parent", "role"], 11 | filter: [{ 12 | field: "user", 13 | op: "=", 14 | value: user.id, 15 | }], 16 | }); 17 | return result.rows.map((row) => { 18 | return { 19 | accountId: row.parent, 20 | role: row.role, 21 | }; 22 | }); 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/orm/migrate/settings-type/settings-migration-plan.ts: -------------------------------------------------------------------------------- 1 | import type { SettingsRow } from "~/orm/settings/types.ts"; 2 | import type { EntryMigrationPlan } from "~/orm/migrate/entry-type/entry-migration-plan.ts"; 3 | 4 | export class SettingsMigrationPlan { 5 | settingsType: string; 6 | 7 | fields: { 8 | create: Array>; 9 | drop: Array>; 10 | modify: Array>; 11 | }; 12 | children: Array; 13 | 14 | constructor(settingsType: string) { 15 | this.settingsType = settingsType; 16 | this.fields = { 17 | create: [], 18 | drop: [], 19 | modify: [], 20 | }; 21 | this.children = []; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/terminal/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | BasicBgColor, 3 | BasicFgColor, 4 | Color256, 5 | ColorRGB, 6 | } from "~/terminal/color-me.ts"; 7 | 8 | export type LineStyle = 9 | | "standard" 10 | | "double" 11 | | "thick" 12 | | "dotted" 13 | | "block" 14 | | "doubleSingle"; 15 | 16 | export interface Theme { 17 | backgroundColor: BasicBgColor; 18 | primaryColor: BasicFgColor; 19 | lineStyle: LineStyle; 20 | } 21 | 22 | export interface StyleOptions { 23 | color?: BasicFgColor | Color256 | ColorRGB; 24 | bgColor?: BasicBgColor | Color256 | ColorRGB; 25 | bold?: boolean; 26 | italic?: boolean; 27 | underline?: boolean; 28 | dim?: boolean; 29 | inverse?: boolean; 30 | strikethrough?: boolean; 31 | blink?: boolean; 32 | } 33 | -------------------------------------------------------------------------------- /src/auth/settings/_auth-settings.type.ts: -------------------------------------------------------------------------------- 1 | import type { SettingsBase } from "@inspatial/cloud/types"; 2 | 3 | export interface AuthSettings extends SettingsBase { 4 | _name: "authSettings"; 5 | /** 6 | * **Google Client ID** (TextField) 7 | * @description The client ID for Google authentication. 8 | * @type {string} 9 | */ 10 | googleClientId?: string; 11 | /** 12 | * **Google Client Secret** (PasswordField) 13 | * @description The client secret for Google authentication. 14 | * @type {string} 15 | */ 16 | googleClientSecret?: string; 17 | /** 18 | * **Hostname** (URLField) 19 | * @description The hostname for the server used to construct the redirect URL. 20 | * @type {string} 21 | */ 22 | hostname?: string; 23 | } 24 | -------------------------------------------------------------------------------- /src/auth/settings/field-groups/google-fields.ts: -------------------------------------------------------------------------------- 1 | import type { InField } from "~/orm/field/field-def-types.ts"; 2 | 3 | export const googleFields: Array = [{ 4 | key: "googleClientId", 5 | type: "TextField", 6 | label: "Google Client ID", 7 | description: "The client ID for Google authentication.", 8 | defaultValue: "", 9 | }, { 10 | key: "googleClientSecret", 11 | type: "PasswordField", 12 | label: "Google Client Secret", 13 | description: "The client secret for Google authentication.", 14 | defaultValue: "", 15 | }, { 16 | key: "hostname", 17 | type: "URLField", 18 | label: "Hostname", 19 | description: 20 | "The hostname for the server used to construct the redirect URL.", 21 | defaultValue: "http://localhost:8000", 22 | }]; 23 | -------------------------------------------------------------------------------- /src/auth/security.ts: -------------------------------------------------------------------------------- 1 | export async function hashPassword( 2 | password: string, 3 | salt?: string, 4 | ): Promise { 5 | const passwordBuffer = new TextEncoder().encode(`${password}${salt || ""}`); 6 | const hashBuffer = await crypto.subtle.digest("SHA-256", passwordBuffer); 7 | const hashArray = Array.from(new Uint8Array(hashBuffer)); 8 | return toHex(new Uint8Array(hashArray)); 9 | } 10 | 11 | export function generateSalt(length: number = 16): string { 12 | const buffer = new Uint8Array(length); 13 | crypto.getRandomValues(buffer); 14 | return toHex(buffer); 15 | } 16 | 17 | function toHex(bytes: Uint8Array): string { 18 | const byteArray = Array.from(bytes); 19 | return byteArray.map((byte) => byte.toString(16).padStart(2, "0")).join(""); 20 | } 21 | -------------------------------------------------------------------------------- /src/orm/utils/ulid.ts: -------------------------------------------------------------------------------- 1 | function encodeBase32(input: number) { 2 | const base32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; 3 | 4 | let output = ""; 5 | let value = input; 6 | while (value > 0) { 7 | const index = value % 32; 8 | output = base32[index] + output; 9 | value = Math.floor(value / 32); 10 | } 11 | return output; 12 | } 13 | 14 | function ulid(): string { 15 | const timestamp = Date.now(); 16 | const timeChars = encodeBase32(timestamp).padStart(10, "0"); 17 | const randomChars = crypto.getRandomValues(new Uint8Array(8)); 18 | const randomChars32 = Array.from(randomChars) 19 | .map((value) => encodeBase32(value).padStart(2, "0")) 20 | .join(""); 21 | const ulid = timeChars + randomChars32; 22 | return ulid; 23 | } 24 | export default ulid; 25 | -------------------------------------------------------------------------------- /cli/src/multicore.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get the number of CPU cores available on the system. 3 | * 4 | * **Note:** This function is currently only implemented for Linux systems. 5 | * If you are using a different OS, it will return 1. 6 | */ 7 | export async function getCoreCount( 8 | config?: { single?: boolean }, 9 | ): Promise { 10 | const { single = false } = config || {}; 11 | if (single) return 1; 12 | if (Deno.build.os !== "linux") { 13 | return 1; 14 | } 15 | 16 | const cmd = new Deno.Command("nproc", { 17 | stdout: "piped", 18 | }); 19 | 20 | const proc = cmd.spawn(); 21 | 22 | const output = await proc.output(); 23 | const cors = new TextDecoder().decode(output.stdout).trim(); 24 | const coreCount = parseInt(cors); 25 | return isNaN(coreCount) ? 1 : coreCount; 26 | } 27 | -------------------------------------------------------------------------------- /src/email/email-manager.ts: -------------------------------------------------------------------------------- 1 | import type { InCloud } from "@inspatial/cloud/types"; 2 | import type { Email } from "./entries/_email.type.ts"; 3 | 4 | export class EmailManager { 5 | constructor(private inCloud: InCloud) { 6 | } 7 | 8 | async sendEmail({ 9 | recipientEmail, 10 | subject, 11 | body, 12 | now, 13 | }: { 14 | recipientEmail: string; 15 | subject: string; 16 | body: string; 17 | now?: boolean; 18 | }): Promise { 19 | const orm = this.inCloud.orm; 20 | const email = await orm.createEntry("email", { 21 | senderName: this.inCloud.appName, 22 | recipientEmail, 23 | subject, 24 | body, 25 | }); 26 | if (now) { 27 | return await email.runAction("send"); 28 | } 29 | return await email.runAction("enqueueSend"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/auth/actions/get-account.ts: -------------------------------------------------------------------------------- 1 | import { CloudAPIAction } from "@inspatial/cloud"; 2 | import type { SessionData } from "../types.ts"; 3 | import type { Account } from "../entries/account/_account.type.ts"; 4 | 5 | export const getAccount = new CloudAPIAction("getAccount", { 6 | description: "Gets the account for the current authenticated user", 7 | async run({ inRequest, inCloud }) { 8 | const user = inRequest.context.get("user"); 9 | if (!user || !user.accountId) { 10 | return; 11 | } 12 | const orm = inCloud.orm.withAccount(user.accountId); 13 | const account = await orm.getEntry("account", user.accountId); 14 | 15 | if (!account.onboardingComplete) { 16 | // const steps = await orm.systemDb.getRows("") 17 | } 18 | return account.data; 19 | }, 20 | params: [], 21 | }); 22 | -------------------------------------------------------------------------------- /src/email/settings/_email-settings.type.ts: -------------------------------------------------------------------------------- 1 | import type { SettingsBase } from "@inspatial/cloud/types"; 2 | 3 | export interface EmailSettings extends SettingsBase { 4 | _name: "emailSettings"; 5 | /** 6 | * **Final Redirect** (URLField) 7 | * @description The final url to redirect to after Google OAuth completes 8 | * @type {string} 9 | */ 10 | redirectFinal?: string; 11 | /** 12 | * **Default Send Account** (ConnectionField) 13 | * 14 | * **EntryType** `emailAccount` 15 | * @description The default email account to use for sending emails 16 | * @type {string} 17 | */ 18 | defaultSendAccount?: string; 19 | /** 20 | * **Default Send Account Title** (EmailField) 21 | * @description The email account to send emails from 22 | * @type {string} 23 | */ 24 | defaultSendAccount__title?: string; 25 | } 26 | -------------------------------------------------------------------------------- /src/auth/entries/user-session/user-session-entry.ts: -------------------------------------------------------------------------------- 1 | import { EntryType } from "~/orm/entry/entry-type.ts"; 2 | 3 | import fields from "~/auth/entries/user-session/fields.ts"; 4 | import { generateSalt } from "~/auth/security.ts"; 5 | import type { UserSession } from "./_user-session.type.ts"; 6 | 7 | export const userSessionEntry = new EntryType("userSession", { 8 | label: "User Session", 9 | description: "An authenticated user session", 10 | systemGlobal: true, 11 | idMode: "ulid", 12 | fields: fields, 13 | actions: [], 14 | defaultListFields: ["user"], 15 | hooks: { 16 | beforeCreate: [{ 17 | name: "setSessionId", 18 | description: "Set the session ID to a unique value", 19 | handler({ userSession }): void { 20 | userSession.sessionId = generateSalt(32); 21 | }, 22 | }], 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /src/orm/migrate/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ForeignKeyConstraint, 3 | PgColumnDefinition, 4 | PgDataTypeDefinition, 5 | } from "~/orm/db/db-types.ts"; 6 | 7 | export interface ColumnMigrationPlan { 8 | columnName: string; 9 | dataType?: { 10 | from: PgDataTypeDefinition; 11 | to: PgDataTypeDefinition; 12 | }; 13 | nullable?: { 14 | from: PgColumnDefinition["isNullable"]; 15 | to: PgColumnDefinition["isNullable"]; 16 | defaultValue?: PgColumnDefinition["columnDefault"]; 17 | }; 18 | unique?: { 19 | from: boolean; 20 | to: boolean; 21 | }; 22 | foreignKey?: { 23 | drop?: string; 24 | create?: ForeignKeyConstraint; 25 | }; 26 | } 27 | 28 | export interface ColumnCreatePlan { 29 | columnName: string; 30 | column: PgColumnDefinition; 31 | foreignKey?: { 32 | create: ForeignKeyConstraint; 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/orm/field/fields/boolean-field.ts: -------------------------------------------------------------------------------- 1 | import { ORMFieldConfig } from "~/orm/field/orm-field.ts"; 2 | 3 | export default new ORMFieldConfig("BooleanField", { 4 | dbColumn: (fieldDef) => { 5 | return { 6 | columnName: fieldDef.key, 7 | dataType: "boolean", 8 | columnDefault: false, 9 | }; 10 | }, 11 | dbLoad(value, _fieldDef) { 12 | return value; 13 | }, 14 | validate(_value, _fieldDef) { 15 | return true; 16 | }, 17 | normalize(value, _fieldDef) { 18 | switch (value) { 19 | case true: 20 | case "true": 21 | case 1: 22 | case "1": 23 | value = true; 24 | break; 25 | case false: 26 | case "false": 27 | case 0: 28 | case "0": 29 | value = false; 30 | } 31 | return value; 32 | }, 33 | dbSave(value, _fieldDef) { 34 | return value; 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /src/orm/build/generate-interface/field-type-map.ts: -------------------------------------------------------------------------------- 1 | import type { InFieldType } from "~/orm/field/field-def-types.ts"; 2 | 3 | export const fieldTypeMap: Record = { 4 | URLField: "string", 5 | BigIntField: "number", 6 | BooleanField: "boolean", 7 | ChoicesField: "string", 8 | ConnectionField: "string", 9 | CurrencyField: "number", 10 | DataField: "string", 11 | DateField: "string", 12 | DecimalField: "number", 13 | EmailField: "string", 14 | ImageField: "string", 15 | IntField: "number", 16 | JSONField: "Record", 17 | ListField: "Array", 18 | TimeStampField: "number", 19 | TextField: "string", 20 | MultiChoiceField: "Array", 21 | PasswordField: "string", 22 | PhoneField: "string", 23 | RichTextField: "string", 24 | IDField: "string", 25 | FileField: "string", 26 | TimeField: "string", 27 | }; 28 | -------------------------------------------------------------------------------- /src/auth/entries/user/actions/set-password.ts: -------------------------------------------------------------------------------- 1 | import type { EntryActionDefinition } from "~/orm/entry/types.ts"; 2 | import type { User } from "../_user.type.ts"; 3 | import { generateSalt, hashPassword } from "../../../security.ts"; 4 | 5 | export const setPassword: EntryActionDefinition = { 6 | key: "setPassword", 7 | description: "Set the user's password", 8 | async action({ user, data }): Promise { 9 | const password = data.password as string; 10 | const salt = generateSalt(); 11 | const hashed = await hashPassword(password, salt); 12 | user.password = `${salt}:${hashed}`; 13 | user.resetPasswordToken = undefined; 14 | await user.save(); 15 | }, 16 | params: [ 17 | { 18 | key: "password", 19 | type: "PasswordField", 20 | label: "Password", 21 | description: "Password to set", 22 | required: true, 23 | }, 24 | ], 25 | }; 26 | -------------------------------------------------------------------------------- /src/auth/auth-lifecycle.ts: -------------------------------------------------------------------------------- 1 | import type { LifecycleHandler } from "~/serve/request-lifecycle.ts"; 2 | 3 | export const authLifecycle: LifecycleHandler = { 4 | name: "parseAuth", 5 | handler(inRequest) { 6 | if (inRequest.method === "OPTIONS") { 7 | return; 8 | } 9 | inRequest.context.register("user", null); 10 | inRequest.context.register("authToken", null); 11 | inRequest.context.register("userSession", null); 12 | const authHeader = inRequest.headers.get("Authorization"); 13 | if (authHeader) { 14 | const parts = authHeader.split(" "); 15 | if (parts.length === 2 && parts[0].toLowerCase() === "bearer") { 16 | inRequest.context.update("authToken", parts[1]); 17 | } 18 | } 19 | const userSessionId = inRequest.cookies.get("userSession"); 20 | if (userSessionId) { 21 | inRequest.context.update("userSession", userSessionId); 22 | } 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /extensions/user-agent/mod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This extension provides user-agent parsing for { @link InSpatialServer } 3 | * @module userAgent 4 | * @example 5 | * ```ts 6 | * import { InSpatialServer } from "@inspatial/serve"; 7 | * import userAgentExtension from "@inspatial/serve/user-agent"; 8 | * 9 | * const server = await InSpatialServer.create({ 10 | * extensions: [userAgentExtension], 11 | * }); 12 | * 13 | * server.run(); 14 | * ``` 15 | */ 16 | 17 | export * from "#extensions/user-agent/src/user-agent.ts"; 18 | export * from "#extensions/user-agent/src/types.ts"; 19 | export * from "#extensions/user-agent/src/matchers.ts"; 20 | export * from "#extensions/user-agent/src/helpers.ts"; 21 | export * from "#extensions/user-agent/src//parse.ts"; 22 | export * from "#extensions/user-agent/src/runtime.ts"; 23 | import { userAgentExtension } from "#extensions/user-agent/src/extension.ts"; 24 | 25 | export default userAgentExtension; 26 | -------------------------------------------------------------------------------- /src/auth/entries/user/actions/validate-password.ts: -------------------------------------------------------------------------------- 1 | import type { EntryActionDefinition } from "~/orm/entry/types.ts"; 2 | import type { User } from "../_user.type.ts"; 3 | import { hashPassword } from "../../../security.ts"; 4 | 5 | export const validatePassword: EntryActionDefinition = { 6 | key: "validatePassword", 7 | label: "Validate Password", 8 | description: "Validate the user's password", 9 | async action({ data, user }): Promise { 10 | const password = data.password as string; 11 | const existingPassword = user.password; 12 | if (!existingPassword) { 13 | return false; 14 | } 15 | const [salt, hashed] = existingPassword.split(":"); 16 | const testHash = await hashPassword(password, salt); 17 | return hashed === testHash; 18 | }, 19 | params: [{ 20 | key: "password", 21 | type: "PasswordField", 22 | label: "Password", 23 | description: "Password to validate", 24 | required: true, 25 | }], 26 | }; 27 | -------------------------------------------------------------------------------- /src/orm/field/fields/connection-field.ts: -------------------------------------------------------------------------------- 1 | import { ORMFieldConfig } from "~/orm/field/orm-field.ts"; 2 | import type { PgColumnDefinition } from "~/orm/db/db-types.ts"; 3 | 4 | export default new ORMFieldConfig("ConnectionField", { 5 | dbColumn: (fieldDef) => { 6 | const pgColumn: PgColumnDefinition = { 7 | columnName: fieldDef.key, 8 | dataType: "text", 9 | }; 10 | switch (fieldDef.connectionIdMode) { 11 | case "ulid": 12 | pgColumn.dataType = "character varying", 13 | pgColumn.characterMaximumLength = 26; 14 | break; 15 | case "auto": 16 | pgColumn.dataType = "integer"; 17 | break; 18 | case "uuid": 19 | pgColumn.dataType = "text"; 20 | break; 21 | } 22 | return pgColumn; 23 | }, 24 | dbLoad(value, _fieldDef) { 25 | return value; 26 | }, 27 | validate(_value, _fieldDef) { 28 | return true; 29 | }, 30 | dbSave(value, _fieldDef) { 31 | return value; 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /src/serve/exeption/cloud-exception.ts: -------------------------------------------------------------------------------- 1 | type CloudExceptionType = "fatal" | "error" | "warning"; 2 | interface CloudExeptionOptions { 3 | scope?: string; 4 | type?: CloudExceptionType; 5 | } 6 | /** 7 | * Custom exception class for InSpatial Cloud 8 | */ 9 | 10 | export class CloudException extends Error { 11 | override name = "CloudException"; 12 | scope: string; 13 | type: CloudExceptionType; 14 | 15 | static isCloudException(e: unknown): e is CloudException { 16 | if (e instanceof CloudException) { 17 | return true; 18 | } 19 | return false; 20 | } 21 | 22 | constructor( 23 | message: string, 24 | options?: CloudExeptionOptions, 25 | ) { 26 | super(message); 27 | this.scope = options?.scope || "unknown"; 28 | this.type = options?.type || "error"; 29 | } 30 | } 31 | 32 | export function raiseCloudException( 33 | message: string, 34 | options?: CloudExeptionOptions, 35 | ): never { 36 | throw new CloudException(message, options); 37 | } 38 | -------------------------------------------------------------------------------- /src/auth/actions/logout.ts: -------------------------------------------------------------------------------- 1 | import { CloudAPIAction } from "~/api/cloud-action.ts"; 2 | 3 | const logout = new CloudAPIAction("logout", { 4 | label: "Logout", 5 | description: "Logout user", 6 | async run({ inCloud, inRequest, inResponse }) { 7 | const sessionId = inRequest.context.get("userSession"); 8 | if (sessionId) { 9 | // use InCloud.orm to access entries that require admin privileges 10 | const userSession = await inCloud.orm.findEntry("userSession", [{ 11 | field: "sessionId", 12 | op: "=", 13 | value: sessionId, 14 | }]); 15 | if (userSession) { 16 | await userSession.delete(); 17 | } 18 | inCloud.inCache.deleteValue("userSession", sessionId); 19 | } 20 | inRequest.context.update("user", null); 21 | inRequest.context.update("userSession", null); 22 | inRequest.context.update("authToken", null); 23 | inResponse.clearCookie("userSession"); 24 | }, 25 | params: [], 26 | }); 27 | 28 | export default logout; 29 | -------------------------------------------------------------------------------- /src/serve/middleware.ts: -------------------------------------------------------------------------------- 1 | import type { InRequest } from "~/serve/in-request.ts"; 2 | import type { InResponse } from "~/serve/in-response.ts"; 3 | import type { InCloud } from "~/in-cloud.ts"; 4 | 5 | /** 6 | * Middleware for InSpatialServer. 7 | */ 8 | export type Middleware = { 9 | /** 10 | * The name of the middleware. 11 | * This should be unique. If a middleware with the same name is added more than once, an error will be thrown, preventing the server from starting. 12 | */ 13 | name: string; 14 | /** 15 | * A description of what the middleware does. 16 | */ 17 | description: string; 18 | /** 19 | * The handler for the middleware. 20 | * If the handler returns a response, the response will be sent to the client immediately, 21 | * skipping any further middleware or request handling. 22 | */ 23 | handler: ( 24 | inCloud: InCloud, 25 | inRequest: InRequest, 26 | inResponse: InResponse, 27 | ) => Promise | void | InResponse | Response; 28 | }; 29 | -------------------------------------------------------------------------------- /src/orm/field/fields/id-field.ts: -------------------------------------------------------------------------------- 1 | import { ORMFieldConfig } from "~/orm/field/orm-field.ts"; 2 | import type { PgColumnDefinition } from "~/orm/db/db-types.ts"; 3 | 4 | export default new ORMFieldConfig("IDField", { 5 | dbColumn: (fieldDef) => { 6 | const pgColumn: PgColumnDefinition = { 7 | columnName: fieldDef.key, 8 | dataType: "text", 9 | isNullable: "NO", 10 | isIdentity: true, 11 | }; 12 | switch (fieldDef.idMode) { 13 | case "ulid": 14 | pgColumn.dataType = "character varying", 15 | pgColumn.characterMaximumLength = 26; 16 | break; 17 | case "auto": 18 | pgColumn.dataType = "integer"; 19 | break; 20 | case "uuid": 21 | pgColumn.dataType = "text"; 22 | break; 23 | } 24 | return pgColumn; 25 | }, 26 | dbLoad(value, _fieldDef) { 27 | return value; 28 | }, 29 | validate(_value, _fieldDef) { 30 | return true; 31 | }, 32 | dbSave(value, _fieldDef) { 33 | return value; 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /src/orm/field/types.ts: -------------------------------------------------------------------------------- 1 | export interface Choice { 2 | key: string; 3 | label: string; 4 | description?: string; 5 | color?: string; 6 | } 7 | 8 | export type InValue = 9 | InValueTypeMap[T]; 10 | 11 | type InValueTypeMap = { 12 | IDField: string; 13 | DataField: string; 14 | IntField: number; 15 | BigIntField: bigint; 16 | DecimalField: number; 17 | DateField: string; 18 | TimeStampField: number; 19 | BooleanField: boolean; 20 | PasswordField: string; 21 | ChoicesField: string | number; 22 | MultiChoiceField: string[]; 23 | TextField: string; 24 | EmailField: string; 25 | ImageField: string; 26 | FileField: string; 27 | JSONField: Record; 28 | PhoneField: string; 29 | ConnectionField: { 30 | id: string; 31 | display: string; 32 | }; 33 | RichTextField: Record; 34 | URLField: string; 35 | ListField: string[]; 36 | CurrencyField: number; 37 | TimeField: string; 38 | }; 39 | 40 | export type IDMode = "uuid" | "ulid" | "auto"; 41 | -------------------------------------------------------------------------------- /src/orm/migrate/cloud-migrator.ts: -------------------------------------------------------------------------------- 1 | import { InCloud } from "~/in-cloud.ts"; 2 | 3 | export class InCloudMigrator extends InCloud { 4 | constructor(appName: string, config: any) { 5 | super(appName, config, "migrator"); 6 | } 7 | async migrate(): Promise { 8 | await this.#migrateGlobal(); 9 | await this.#migrateAccounts(); 10 | } 11 | 12 | async #migrateGlobal() { 13 | const orm = this.orm.withUser(this.orm.systemGobalUser); 14 | await orm.migrateGlobal(); 15 | for (const migrateAction of this.extensionManager.afterMigrate.global) { 16 | await migrateAction.action({ 17 | inCloud: this, 18 | orm, 19 | }); 20 | } 21 | } 22 | 23 | async #migrateAccounts() { 24 | const { rows: accounts } = await this.orm.getEntryList( 25 | "account", 26 | { 27 | columns: ["id"], 28 | filter: { 29 | initialized: true, 30 | }, 31 | limit: 0, 32 | }, 33 | ); 34 | for (const { id } of accounts) { 35 | await this.orm.migrate(id); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/auth/actions/set-new-password.ts: -------------------------------------------------------------------------------- 1 | import { CloudAPIAction } from "~/api/cloud-action.ts"; 2 | import { raiseServerException } from "~/serve/server-exception.ts"; 3 | 4 | export const setNewPassword = new CloudAPIAction("setNewPassword", { 5 | description: "Reset user password", 6 | authRequired: false, 7 | async run({ orm, params }) { 8 | const { token, password } = params; 9 | const user = await orm.findEntry("user", [{ 10 | field: "resetPasswordToken", 11 | op: "=", 12 | value: token, 13 | }]); 14 | if (!user) { 15 | raiseServerException(400, "Invalid token"); 16 | } 17 | await user.runAction("setPassword", { password }); 18 | return { 19 | status: "success", 20 | }; 21 | }, 22 | params: [{ 23 | key: "token", 24 | label: "Token", 25 | description: "Token to reset password", 26 | type: "TextField", 27 | required: true, 28 | }, { 29 | key: "password", 30 | label: "Password", 31 | description: "New password to set", 32 | type: "PasswordField", 33 | required: true, 34 | }], 35 | }); 36 | -------------------------------------------------------------------------------- /examples/crm/entryTypes/customerAccount.ts: -------------------------------------------------------------------------------- 1 | import { ChildEntryType, EntryType } from "@inspatial/cloud"; 2 | 3 | export const customerAccount = new EntryType( 4 | "customerAccount", 5 | { 6 | label: "Customer Account", 7 | idMode: "ulid", 8 | titleField: "customerName", 9 | fields: [ 10 | { 11 | key: "customerName", 12 | type: "DataField", 13 | label: "Customer Name", 14 | required: true, 15 | }, 16 | { 17 | key: "customerId", 18 | type: "DataField", 19 | label: "Customer ID", 20 | required: true, 21 | }, 22 | ], 23 | children: [ 24 | new ChildEntryType("users", { 25 | description: "Users associated with this account", 26 | label: "Users", 27 | fields: [{ 28 | key: "user", 29 | label: "User", 30 | type: "ConnectionField", 31 | entryType: "user", 32 | }, { 33 | key: "isOwner", 34 | label: "Is Owner", 35 | type: "BooleanField", 36 | }], 37 | }), 38 | ], 39 | }, 40 | ); 41 | -------------------------------------------------------------------------------- /src/extension/core-config.ts: -------------------------------------------------------------------------------- 1 | import type { CurrencyCode } from "../orm/field/field-def-types.ts"; 2 | 3 | export type CoreConfig = { 4 | name: string; 5 | cloudMode: "production" | "development"; 6 | logLevel: "info" | "debug" | "error" | "warn"; 7 | logTrace: boolean; 8 | brokerPort: number; 9 | queuePort: number; 10 | hostName: string; 11 | port: number; 12 | autoConfig: boolean; 13 | allowedOrigins: Set; 14 | publicRoot: string; 15 | singlePageApp: boolean; 16 | spaRootPaths: Set; 17 | cacheStatic: boolean; 18 | autoTypes: boolean; 19 | autoMigrate: boolean; 20 | embeddedDb: boolean; 21 | embeddedDbPort: number; 22 | ormDebugMode: boolean; 23 | dbConnectionType: "tcp" | "socket"; 24 | dbSocketPath: string; 25 | dbName: string; 26 | dbCurrency: "en_US.UTF-8" | "en_GB.UTF-8"; 27 | dbHost: string; 28 | dbPort: number; 29 | dbUser: string; 30 | dbPassword: string; 31 | dbAppName: string; 32 | dbClientMode: "pool" | "single"; 33 | dbPoolSize: number; 34 | dbMaxPoolSize: number; 35 | dbIdleTimeout: number; 36 | authAllowAll: boolean; 37 | defaultCurrency: CurrencyCode; 38 | }; 39 | -------------------------------------------------------------------------------- /src/files/actions/get-file.ts: -------------------------------------------------------------------------------- 1 | import { CloudAPIAction } from "~/api/cloud-action.ts"; 2 | 3 | import { raiseServerException } from "~/serve/server-exception.ts"; 4 | import type { CloudFile } from "../entries/_cloud-file.type.ts"; 5 | 6 | export const getFile = new CloudAPIAction("getFile", { 7 | params: [{ 8 | key: "fileId", 9 | label: "File ID", 10 | type: "DataField", 11 | required: true, 12 | }, { 13 | key: "download", 14 | label: "Download", 15 | type: "BooleanField", 16 | }], 17 | async run({ orm, params, inResponse }) { 18 | const { fileId } = params; 19 | const file = await orm.getEntry("cloudFile", fileId); 20 | try { 21 | const fileHandle = await Deno.open(file.filePath, { read: true }); 22 | 23 | inResponse.setFile({ 24 | content: fileHandle.readable, 25 | fileName: file.fileName, 26 | download: params.download, 27 | }); 28 | return inResponse.respond(); 29 | } catch (e) { 30 | if (e instanceof Deno.errors.NotFound) { 31 | raiseServerException(404, `File not found: ${file.fileName}`); 32 | } 33 | } 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /src/email/actions/sendEmail.ts: -------------------------------------------------------------------------------- 1 | import { CloudAPIAction } from "@inspatial/cloud"; 2 | 3 | export const sendEmail = new CloudAPIAction("sendEmail", { 4 | label: "Send Email", 5 | authRequired: true, 6 | description: "Send an email to a recipient", 7 | params: [ 8 | { 9 | key: "recipientEmail", 10 | type: "EmailField", 11 | label: "Recipient Email", 12 | required: true, 13 | }, 14 | 15 | { 16 | key: "subject", 17 | label: "Subject", 18 | type: "TextField", 19 | required: true, 20 | }, 21 | { 22 | key: "body", 23 | label: "Body", 24 | type: "TextField", 25 | required: true, 26 | }, 27 | { 28 | key: "now", 29 | label: "Send Immediately", 30 | description: 31 | "Sends the email immediately, instead of adding it to the task queue", 32 | type: "BooleanField", 33 | }, 34 | ], 35 | async run({ inCloud, params }) { 36 | const { body, recipientEmail, subject, now } = params; 37 | return await inCloud.emailManager.sendEmail({ 38 | body, 39 | recipientEmail, 40 | subject, 41 | now, 42 | }); 43 | }, 44 | }); 45 | -------------------------------------------------------------------------------- /extensions/user-agent/src/extension.ts: -------------------------------------------------------------------------------- 1 | import { CloudExtension } from "~/extension/cloud-extension.ts"; 2 | import { parseUserAgent } from "#extensions/user-agent/src/parse.ts"; 3 | 4 | export const userAgentExtension = new CloudExtension("userAgent", { 5 | label: "User Agent", 6 | description: "This extension provides user-agent parsing", 7 | config: { 8 | userAgentDebug: { 9 | type: "boolean", 10 | description: "Enable user agent printing to console", 11 | default: true, 12 | }, 13 | userAgentEnabled: { 14 | type: "boolean", 15 | description: "Enable user agent parsing", 16 | default: true, 17 | }, 18 | }, 19 | requestLifecycle: { 20 | setup: [{ 21 | name: "parseUserAgent", 22 | handler(inRequest, config) { 23 | if (!config.userAgentEnabled) return; 24 | 25 | const agent = parseUserAgent(inRequest.headers.get("user-agent")); 26 | inRequest.context.register("userAgent", agent); 27 | 28 | if (config.userAgentDebug) { 29 | const userAgent = inRequest.context.get("userAgent"); 30 | console.log({ userAgent }); 31 | } 32 | }, 33 | }], 34 | }, 35 | install() {}, 36 | }) as CloudExtension; 37 | -------------------------------------------------------------------------------- /src/orm/migrate/migration-plan.ts: -------------------------------------------------------------------------------- 1 | import type { EntryMigrationPlan } from "~/orm/migrate/entry-type/entry-migration-plan.ts"; 2 | import type { SettingsMigrationPlan } from "~/orm/migrate/settings-type/settings-migration-plan.ts"; 3 | 4 | export class MigrationPlan { 5 | summary: { 6 | createTables: number; 7 | dropTables: number; 8 | addColumns: number; 9 | modifyColumns: number; 10 | dropColumns: number; 11 | addSettingsFields: number; 12 | modifySettingsFields: number; 13 | dropSettingsFields: number; 14 | }; 15 | database: string; 16 | schema: string; 17 | settingsTable: { 18 | create: boolean; 19 | }; 20 | entries: Array; 21 | settings: Array; 22 | 23 | constructor() { 24 | this.summary = { 25 | createTables: 0, 26 | dropTables: 0, 27 | addColumns: 0, 28 | modifyColumns: 0, 29 | dropColumns: 0, 30 | addSettingsFields: 0, 31 | modifySettingsFields: 0, 32 | dropSettingsFields: 0, 33 | }; 34 | this.database = ""; 35 | this.schema = ""; 36 | this.settingsTable = { 37 | create: false, 38 | }; 39 | this.entries = []; 40 | this.settings = []; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/serve/cloud-server.ts: -------------------------------------------------------------------------------- 1 | import type { CloudConfig } from "#types/mod.ts"; 2 | import { InCloud } from "~/in-cloud.ts"; 3 | import { requestHandler } from "~/serve/request-handler.ts"; 4 | 5 | export class InCloudServer extends InCloud { 6 | instanceNumber: string; 7 | constructor( 8 | appName: string, 9 | config: CloudConfig, 10 | instanceNumber?: string, 11 | ) { 12 | super(appName, config, "server"); 13 | this.instanceNumber = instanceNumber || "_"; 14 | } 15 | 16 | override async run() { 17 | await super.run(); 18 | this.#serve(); 19 | } 20 | 21 | #serve(): Deno.HttpServer { 22 | const reusePort = Deno.env.get("REUSE_PORT") === "true"; 23 | const config = this.getExtensionConfig("core"); 24 | 25 | return Deno.serve( 26 | { 27 | hostname: config.hostName, 28 | port: config.port, 29 | reusePort, 30 | onListen: (_addr) => { 31 | // Hide stdout message 32 | }, 33 | }, 34 | this.#requestHandler.bind(this), 35 | ); 36 | } 37 | async #requestHandler(request: Request): Promise { 38 | return await requestHandler( 39 | request, 40 | this, 41 | this.extensionManager, 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/auth/actions/update-account.ts: -------------------------------------------------------------------------------- 1 | import { CloudAPIAction } from "../../api/cloud-action.ts"; 2 | import type { Account } from "../entries/account/_account.type.ts"; 3 | import type { SessionData } from "../types.ts"; 4 | 5 | export const updateAccount = new CloudAPIAction("updateAccount", { 6 | description: "Updates the account data for the current authenticated user", 7 | async run({ inRequest, params, inCloud }) { 8 | const user = inRequest.context.get("user"); 9 | if (!user || !user.accountId) { 10 | return; 11 | } 12 | const { accountData } = params; 13 | const orm = inCloud.orm.withAccount(user.accountId); 14 | const account = await orm.getEntry("account", user.accountId); 15 | 16 | for (const [key, value] of Object.entries(accountData)) { 17 | switch (key) { 18 | case "onboardingComplete": 19 | account.onboardingComplete = value as boolean; 20 | break; 21 | default: 22 | account.update({ 23 | [key]: value, 24 | }); 25 | } 26 | } 27 | await account.save(); 28 | }, 29 | params: [{ 30 | key: "accountData", 31 | type: "JSONField", 32 | required: true, 33 | description: "The account data to update", 34 | }], 35 | }); 36 | -------------------------------------------------------------------------------- /src/email/settings/emailSettings.ts: -------------------------------------------------------------------------------- 1 | import { SettingsType } from "~/orm/settings/settings-type.ts"; 2 | import type { EmailSettings } from "./_email-settings.type.ts"; 3 | 4 | export const emailSettings: SettingsType = new SettingsType< 5 | EmailSettings 6 | >("emailSettings", { 7 | label: "Email Settings", 8 | description: "Settings for sending emails", 9 | systemGlobal: true, 10 | fields: [ 11 | { 12 | key: "redirectFinal", 13 | label: "Final Redirect", 14 | type: "URLField", 15 | description: "The final url to redirect to after Google OAuth completes", 16 | }, 17 | { 18 | key: "defaultSendAccount", 19 | label: "Default Send Account", 20 | type: "ConnectionField", 21 | entryType: "emailAccount", 22 | description: "The default email account to use for sending emails", 23 | }, 24 | ], 25 | fieldGroups: [ 26 | { 27 | key: "google", 28 | label: "Google Settings", 29 | description: "Settings for Google OAuth", 30 | fields: [ 31 | "redirectFinal", 32 | ], 33 | }, 34 | { 35 | key: "smtp", 36 | label: "SMTP Settings", 37 | description: "Settings for SMTP server", 38 | fields: [ 39 | "defaultSendAccount", 40 | ], 41 | }, 42 | ], 43 | }); 44 | -------------------------------------------------------------------------------- /src/api/api-handler.ts: -------------------------------------------------------------------------------- 1 | import type { HandlerResponse, PathHandler } from "~/serve/path-handler.ts"; 2 | import { raiseServerException, Redirect } from "~/serve/server-exception.ts"; 3 | 4 | export const apiPathHandler: PathHandler = { 5 | name: "api", 6 | description: "api", 7 | match: /^\/api$/, 8 | handler: async (inCloud, inRequest, inResponse) => { 9 | const { api } = inCloud; 10 | const groupParam = inRequest.group; 11 | const actionParam = inRequest.action; 12 | if (!groupParam) { 13 | return api.docs as HandlerResponse; 14 | } 15 | const action = api.getAction(groupParam, actionParam); 16 | 17 | if (!action) { 18 | raiseServerException( 19 | 404, 20 | `Action not found for Group: '${groupParam}', Action: '${actionParam}'`, 21 | ); 22 | } 23 | let data: any = {}; 24 | switch (action.raw) { 25 | case true: 26 | data = Object.fromEntries(inRequest.params); 27 | break; 28 | default: 29 | data = await inRequest.body; 30 | } 31 | const result = await action.run({ 32 | inCloud, 33 | inRequest, 34 | inResponse, 35 | params: data, 36 | }); 37 | if (result instanceof Redirect) { 38 | return inResponse.redirect(result.url); 39 | } 40 | return result; 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@inspatial/cloud", 3 | "version": "0.5.7", 4 | "license": "Apache-2.0", 5 | "exports": { 6 | ".": "./mod.ts", 7 | "./extensions": "./extensions/mod.ts", 8 | "./types": "./types.ts", 9 | "./incloud": "./cli/incloud.ts" 10 | }, 11 | 12 | "publish": { 13 | "include": [ 14 | "src/", 15 | "src/**/*.wasm", 16 | "src/**/*.data", 17 | "examples/", 18 | "extensions/", 19 | "cli/", 20 | "types.ts", 21 | "mod.ts", 22 | "README.md", 23 | "LICENSE" 24 | ], 25 | "exclude": [".github/", ".vscode/", ".zed/", "examples/**/.inspatial/"] 26 | }, 27 | "imports": { 28 | "~/": "./src/", 29 | "#cli/": "./cli/src/", 30 | "#extensions/": "./extensions/", 31 | "#types/": "./src/types/", 32 | "#inLog": "./src/in-log/in-log.ts" 33 | }, 34 | "tasks": { 35 | "check": "deno publish --dry-run", 36 | "type-check": "deno check --all mod.ts", 37 | "example:tasks": "cd examples/task-queue && deno run -A --unstable-broadcast-channel run.ts", 38 | "example:tasks-queue": "cd examples/task-queue && deno run -A --unstable-broadcast-channel queue.ts" 39 | }, 40 | "lint": { 41 | "rules": { 42 | "include": ["verbatim-module-syntax"], 43 | "exclude": ["no-explicit-any"] 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/email/smtp/smtpTypes.ts: -------------------------------------------------------------------------------- 1 | export type State = 2 | | "notStarted" 3 | | "connecting" 4 | | "connected" 5 | | "tlsChecking" 6 | | "tlsCapable" 7 | | "tlsReady" 8 | | "tlsConnecting" 9 | | "tlsConnected" 10 | | "sayingHello" 11 | | "authReady" 12 | | "authenticating" 13 | | "authUsername" 14 | | "authPassword" 15 | | "authenticated" 16 | | "dataReady" 17 | | "disconnecting" 18 | | "disconnect"; 19 | 20 | export interface SMTPCapabilities { 21 | PIPELINING: boolean; 22 | STARTTLS: boolean; 23 | SMTPUTF8: boolean; 24 | AUTH: { 25 | LOGIN: boolean; 26 | PLAIN: boolean; 27 | CRAM_MD5: boolean; 28 | XOAUTH2: boolean; 29 | PLAIN_CLIENTTOKEN: boolean; 30 | OAUTHBEARER: boolean; 31 | XOAUTH: boolean; 32 | }; 33 | "8BITMIME": boolean; 34 | ENHANCEDSTATUSCODES: boolean; 35 | CHUNKING: boolean; 36 | SIZE: number; 37 | } 38 | 39 | export interface SMTPHeader { 40 | from: string; 41 | to: string; 42 | subject: string; 43 | } 44 | 45 | export type SMTPCommand = 46 | | "HELO" 47 | | "EHLO" 48 | | "MAIL" 49 | | "RCPT" 50 | | "DATA" 51 | | "QUIT" 52 | | "AUTH" 53 | | "STARTTLS"; 54 | 55 | export interface SMTPOptions { 56 | smtpServer: string; 57 | port: number; 58 | userLogin: string; 59 | password: string; 60 | 61 | authMethod?: "PLAIN" | "LOGIN" | "XOAUTH2"; 62 | 63 | domain: string; 64 | } 65 | -------------------------------------------------------------------------------- /src/orm/db/postgres/in-pg/types.ts: -------------------------------------------------------------------------------- 1 | import type { MemFile } from "./src/fileManager/pg-file.ts"; 2 | 3 | export interface PGFileBase { 4 | path: string; 5 | info?: Deno.FileInfo; 6 | fd: number; 7 | isMem: boolean; 8 | } 9 | export interface PGFile extends PGFileBase { 10 | file: Deno.FsFile; 11 | isMem: false; 12 | } 13 | export interface PGFileMem extends PGFileBase { 14 | file: MemFile; 15 | devType: DevType | null; 16 | isMem: true; 17 | } 18 | export type DevType = "shm" | "tty" | "tmp" | "urandom"; 19 | 20 | export interface InPgOptions { 21 | env: Record; 22 | args: Array; 23 | debug?: boolean; 24 | onStderr?: (out: Output | OutputMore) => void; 25 | onStdout?: (out: Output | OutputMore) => void; 26 | } 27 | 28 | export interface Output { 29 | message: string; 30 | } 31 | export interface OutputMore { 32 | message: string; 33 | type: string; 34 | date: string; 35 | time: string; 36 | } 37 | 38 | export type WasmImports = Record< 39 | string, 40 | unknown 41 | >; 42 | export interface DLMetaData { 43 | neededDynlibs: Array; 44 | tlsExports: Set; 45 | weakImports: Set; 46 | memorySize: number; 47 | memoryAlign: number; 48 | tableSize: number; 49 | tableAlign: number; 50 | } 51 | export interface DSO { 52 | refcount: number; 53 | name: string; 54 | exports: any; 55 | global: boolean; 56 | } 57 | -------------------------------------------------------------------------------- /src/auth/actions/login.ts: -------------------------------------------------------------------------------- 1 | import { CloudAPIAction } from "~/api/cloud-action.ts"; 2 | 3 | import { raiseServerException } from "~/serve/server-exception.ts"; 4 | import type { User } from "~/auth/entries/user/_user.type.ts"; 5 | 6 | const login = new CloudAPIAction("login", { 7 | label: "Login", 8 | description: "Login to the system", 9 | authRequired: false, 10 | async run({ inCloud, orm, inRequest, inResponse, params }) { 11 | const { email, password } = params; 12 | 13 | const user = await orm.findEntry("user", [{ 14 | field: "email", 15 | op: "=", 16 | value: email, 17 | }]); 18 | if (!user) { 19 | raiseServerException(401, "unauthorized"); 20 | } 21 | const isValid = await user.runAction("validatePassword", { 22 | password, 23 | }); 24 | if (!isValid) { 25 | raiseServerException(401, "unauthorized"); 26 | } 27 | const authHandler = inCloud.auth; 28 | return await authHandler.createUserSession(user, inRequest, inResponse); 29 | }, 30 | params: [{ 31 | key: "email", 32 | label: "Email", 33 | type: "EmailField", 34 | description: "The email of the user", 35 | required: true, 36 | }, { 37 | key: "password", 38 | label: "Password", 39 | type: "PasswordField", 40 | description: "The password of the user", 41 | required: true, 42 | }], 43 | }); 44 | 45 | export default login; 46 | -------------------------------------------------------------------------------- /src/orm/db/postgres/pgUtils.ts: -------------------------------------------------------------------------------- 1 | interface MemorySettings { 2 | sharedBuffers: number; 3 | workMem: number; 4 | maintenanceWorkMem: number; 5 | maxConnections: number; 6 | effectiveCacheSize: number; 7 | } 8 | export function calculateMemorySettings( 9 | maxConnections?: number, 10 | ): MemorySettings { 11 | const settings: MemorySettings = { 12 | sharedBuffers: 0, 13 | workMem: 0, 14 | maintenanceWorkMem: 0, 15 | maxConnections: maxConnections || 100, 16 | effectiveCacheSize: 0, 17 | }; 18 | const { total } = getSystemRam(); 19 | settings.sharedBuffers = Math.floor(total * 0.25); 20 | settings.workMem = Math.floor(total * 0.25 / settings.maxConnections); 21 | settings.maintenanceWorkMem = Math.floor(total * 0.05); 22 | settings.effectiveCacheSize = Math.floor(total * 0.5); 23 | return settings; 24 | } 25 | 26 | function getSystemRam() { 27 | const memInfo = Deno.systemMemoryInfo(); 28 | const memory: Record = { 29 | total: 0, 30 | free: 0, 31 | available: 0, 32 | buffers: 0, 33 | cached: 0, 34 | swapTotal: 0, 35 | swapFree: 0, 36 | }; 37 | Object.keys(memInfo).forEach((key) => { 38 | const id = key as keyof Deno.SystemMemoryInfo; 39 | memory[id] = convertToMB(memInfo[id]); 40 | }); 41 | return memory; 42 | } 43 | 44 | function convertToMB(bytes: number) { 45 | return bytes / 1024 / 1024; 46 | } 47 | -------------------------------------------------------------------------------- /src/auth/auth-middleware.ts: -------------------------------------------------------------------------------- 1 | import type { Middleware } from "~/serve/middleware.ts"; 2 | export const authMiddleware: Middleware = { 3 | name: "auth", 4 | description: "Auth middleware", 5 | async handler(inCloud, inRequest, inResponse) { 6 | if (inRequest.method === "OPTIONS") { 7 | return; 8 | } 9 | 10 | const sessionId = inRequest.context.get("userSession"); 11 | 12 | const authHandler = inCloud.auth; 13 | 14 | let sessionData = await authHandler.loadUserSession(sessionId); 15 | if (!sessionData) { 16 | const authToken = inRequest.context.get("authToken"); 17 | sessionData = await authHandler.loadSessionFromToken(authToken); 18 | } 19 | if (sessionData) { 20 | inRequest.context.update("user", sessionData); 21 | return; 22 | } 23 | 24 | let isAllowed = false; 25 | isAllowed = inCloud.getExtensionConfigValue("core", "authAllowAll"); 26 | if (isAllowed) { 27 | return; 28 | } 29 | const path = inRequest.path; 30 | isAllowed = authHandler.isPathAllowed(path); 31 | 32 | const group = inRequest.context.get("apiGroup"); 33 | const action = inRequest.context.get("apiAction"); 34 | 35 | if (group && action) { 36 | isAllowed = authHandler.isActionAllowed(group, action); 37 | } 38 | 39 | if (!isAllowed) { 40 | inResponse.setErrorStatus(403, "Forbidden"); 41 | return inResponse.error(); 42 | } 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /src/in-live/in-live-broker.ts: -------------------------------------------------------------------------------- 1 | import { generateId } from "~/utils/misc.ts"; 2 | 3 | export class InLiveBroker { 4 | clients: Map; 5 | port: number; 6 | constructor(port: number) { 7 | this.port = port; 8 | this.clients = new Map(); 9 | } 10 | 11 | run() { 12 | Deno.serve({ 13 | port: this.port, 14 | hostname: "127.0.0.1", 15 | onListen: (_addr) => { 16 | // hide stdout message 17 | }, 18 | }, (request) => { 19 | const { response, socket } = Deno.upgradeWebSocket(request); 20 | this.addClient(socket); 21 | return response; 22 | }); 23 | } 24 | 25 | addClient(socket: WebSocket) { 26 | const clientId = generateId(); 27 | socket.addEventListener("open", () => { 28 | this.clients.set(clientId, socket); 29 | }); 30 | socket.addEventListener("message", (event) => { 31 | this.handleMessage(clientId, event.data); 32 | }); 33 | socket.addEventListener("close", () => { 34 | this.clients.delete(clientId); 35 | }); 36 | socket.addEventListener("error", (_error) => { 37 | this.clients.delete(clientId); 38 | }); 39 | } 40 | handleMessage(clientId: string, data: string) { 41 | for (const [id, socket] of this.clients.entries()) { 42 | if (id === clientId) { 43 | continue; 44 | } 45 | socket.send(data); 46 | } 47 | 48 | // Handle the message as needed 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/auth/entries/user/fields/google-fields.ts: -------------------------------------------------------------------------------- 1 | import type { InField } from "~/orm/field/field-def-types.ts"; 2 | 3 | export const googleFields: Array = [{ 4 | key: "googleAccessToken", 5 | type: "PasswordField", 6 | label: "Access Token", 7 | description: "The access token used to authenticate the user with Google.", 8 | readOnly: true, 9 | hidden: true, 10 | }, { 11 | key: "googleRefreshToken", 12 | type: "PasswordField", 13 | label: "Refresh Token", 14 | description: "The refresh token used to refresh the access token.", 15 | readOnly: true, 16 | hidden: true, 17 | }, { 18 | key: "googleCredential", 19 | type: "JSONField", 20 | label: "Google Credential", 21 | description: "The credential used to authenticate the user with Google.", 22 | readOnly: true, 23 | hidden: true, 24 | }, { 25 | key: "googleId", 26 | type: "TextField", 27 | label: "Google ID", 28 | description: "The user's Google ID.", 29 | readOnly: true, 30 | hidden: true, 31 | }, { 32 | key: "googlePicture", 33 | type: "URLField", 34 | label: "Google Picture", 35 | description: "The user's Google profile picture.", 36 | readOnly: true, 37 | }, { 38 | key: "googleAuthStatus", 39 | type: "ChoicesField", 40 | label: "Google Auth Status", 41 | readOnly: true, 42 | choices: [{ 43 | key: "authenticated", 44 | label: "Authenticated", 45 | }, { 46 | key: "notAuthenticated", 47 | label: "Not Authenticated", 48 | }], 49 | }]; 50 | -------------------------------------------------------------------------------- /.zed/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "lsp": { 3 | "deno": { 4 | "settings": { 5 | "deno": { 6 | "enable": true, 7 | "suggest": { 8 | "completeFunctionCalls": true, 9 | "names": { 10 | "enabled": true 11 | }, 12 | "paths": { 13 | "enabled": true 14 | }, 15 | "autoImports": { 16 | "enabled": true 17 | }, 18 | "imports": { 19 | "autoDiscover": true 20 | } 21 | }, 22 | "inlayHints": { 23 | "parameterNames": { 24 | "enabled": "all" 25 | }, 26 | "parameterTypes": { 27 | "enabled": true 28 | }, 29 | "variableTypes": { 30 | "enabled": true 31 | }, 32 | "propertyDeclarationTypes": { 33 | "enabled": true 34 | }, 35 | "functionLikeReturnTypes": { 36 | "enabled": true 37 | }, 38 | "enumMemberValues": { 39 | "enabled": true 40 | } 41 | } 42 | } 43 | } 44 | } 45 | }, 46 | "languages": { 47 | "TypeScript": { 48 | "language_servers": ["deno"], 49 | "formatter": "language_server" 50 | }, 51 | "JavaScript": { 52 | "language_servers": ["deno"], 53 | "formatter": "language_server" 54 | // "prettier": null 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/auth/entries/user-session/_user-session.type.ts: -------------------------------------------------------------------------------- 1 | import type { EntryBase } from "@inspatial/cloud/types"; 2 | 3 | export interface UserSession extends EntryBase { 4 | _name: "userSession"; 5 | /** 6 | * **User** (ConnectionField) 7 | * 8 | * **EntryType** `user` 9 | * @description The user associated with this session 10 | * @type {string} 11 | * @required true 12 | */ 13 | user: string; 14 | /** 15 | * **Session ID** (DataField) 16 | * @description Unique identifier for the session 17 | * @type {string} 18 | * @required true 19 | */ 20 | sessionId: string; 21 | /** 22 | * **Session Data** (JSONField) 23 | * @description Data associated with the session 24 | * @type {Record} 25 | */ 26 | sessionData?: Record; 27 | /** 28 | * **User Session** (IDField) 29 | * @type {string} 30 | * @required true 31 | */ 32 | id: string; 33 | /** 34 | * **Created At** (TimeStampField) 35 | * @description The date and time this entry was created 36 | * @type {number} 37 | * @required true 38 | */ 39 | createdAt: number; 40 | /** 41 | * **Updated At** (TimeStampField) 42 | * @description The date and time this entry was last updated 43 | * @type {number} 44 | * @required true 45 | */ 46 | updatedAt: number; 47 | /** 48 | * **User Title** (DataField) 49 | * @description The user's full name (automatically generated) 50 | * @type {string} 51 | */ 52 | user__title?: string; 53 | } 54 | -------------------------------------------------------------------------------- /src/serve/request-lifecycle.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ConfigDefinition, 3 | ExtensionConfig, 4 | } from "../cloud-config/config-types.ts"; 5 | import type { InRequest } from "~/serve/in-request.ts"; 6 | 7 | /** 8 | * The lifecycle handlers for the incoming requests. 9 | */ 10 | export interface RequestLifecycle< 11 | C extends ConfigDefinition = ConfigDefinition, 12 | > { 13 | /** Setup handlers run before the request is processed by any middleware or path handlers. */ 14 | setup: Array>; 15 | /** Cleanup handlers run after all middleware and path handlers */ 16 | cleanup: Array>; 17 | } 18 | 19 | /** 20 | * A lifecycle handler for the incoming requests. 21 | */ 22 | export interface LifecycleHandler< 23 | C extends ConfigDefinition = ConfigDefinition, 24 | > { 25 | /** The name of the lifecycle handler. */ 26 | name: string; 27 | /** A brief description of the lifecycle handler. */ 28 | description?: string; 29 | /** The function to run for the lifecycle handler. */ 30 | handler: ( 31 | inRequest: InRequest, 32 | config: ExtensionConfig, 33 | ) => Promise | void; 34 | } 35 | 36 | export interface LifecycleHandlerRunner { 37 | setup: Array<{ 38 | name: string; 39 | description?: string; 40 | handler: (inRequest: InRequest) => Promise | void; 41 | }>; 42 | cleanup: Array<{ 43 | name: string; 44 | description?: string; 45 | handler: (inRequest: InRequest) => Promise | void; 46 | }>; 47 | } 48 | -------------------------------------------------------------------------------- /cli/src/cloud-config.ts: -------------------------------------------------------------------------------- 1 | import { joinPath } from "~/utils/path-utils.ts"; 2 | import convertString from "~/utils/convert-string.ts"; 3 | import type { BuiltInConfig } from "./config-types.ts"; 4 | 5 | /** 6 | * Checks for a cloud-config.json file in the current working directory and loads it to the environment variables. 7 | */ 8 | export function loadCloudConfigFile( 9 | cloudRoot: string, 10 | ): { config: BuiltInConfig; env: Record } | false { 11 | const builtInConfig: Record> = {}; 12 | const env: Record = {}; 13 | try { 14 | const filePath = joinPath(cloudRoot, "cloud-config.json"); 15 | const file = Deno.readTextFileSync(filePath); 16 | const config = JSON.parse(file); 17 | for (const key in config) { 18 | if (key.startsWith("$")) { 19 | continue; 20 | } 21 | const extensionConfig = config[key]; 22 | for (const subKey in extensionConfig) { 23 | if (!Object.keys(builtInConfig).includes(key)) { 24 | builtInConfig[key] = {}; 25 | } 26 | builtInConfig[key][convertString(subKey, "camel")] = 27 | extensionConfig[subKey]; 28 | env[subKey] = extensionConfig[subKey].toString(); 29 | } 30 | } 31 | // TODO: Validate the config against the schema 32 | return { config: builtInConfig as unknown as BuiltInConfig, env }; 33 | } catch (e) { 34 | if (e instanceof Deno.errors.NotFound) { 35 | return false; 36 | } 37 | throw e; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/auth/auth-group.ts: -------------------------------------------------------------------------------- 1 | import { CloudAPIGroup } from "~/api/cloud-group.ts"; 2 | import { resetPassword } from "./actions/reset-password.ts"; 3 | import { setNewPassword } from "./actions/set-new-password.ts"; 4 | import login from "./actions/login.ts"; 5 | import logout from "./actions/logout.ts"; 6 | import authCheck from "./actions/auth-check.ts"; 7 | import registerUser from "./actions/register-user.ts"; 8 | import { signInWithGoogle } from "./actions/google/login-google.ts"; 9 | import { signupWithGoogle } from "./actions/google/signup-google.ts"; 10 | import googleAuthCallback from "./actions/google/google-auth-callback.ts"; 11 | import googleTokenLogin from "./actions/google/google-token-login.ts"; 12 | 13 | import { getAccount } from "./actions/get-account.ts"; 14 | import { updateAccount } from "./actions/update-account.ts"; 15 | import { completeOnboarding } from "../onboarding/actions/complete-onboarding.ts"; 16 | import { registerAccount } from "./actions/register-account.ts"; 17 | 18 | const authGroup = new CloudAPIGroup("auth", { 19 | description: "User, Account and Authentication related actions", 20 | label: "Authentication", 21 | actions: [ 22 | login, 23 | logout, 24 | authCheck, 25 | resetPassword, 26 | setNewPassword, 27 | registerUser, 28 | signInWithGoogle, 29 | signupWithGoogle, 30 | googleAuthCallback, 31 | googleTokenLogin, 32 | getAccount, 33 | updateAccount, 34 | registerAccount, 35 | completeOnboarding, 36 | ], 37 | }); 38 | 39 | export default authGroup; 40 | -------------------------------------------------------------------------------- /src/terminal/terminal.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type BasicFgColor, 3 | ColorMe, 4 | type StyleOptions, 5 | } from "~/terminal/color-me.ts"; 6 | 7 | export class Terminal { 8 | static write(content: string) { 9 | const encoder = new TextEncoder(); 10 | Deno.stdout.write(encoder.encode(content)); 11 | } 12 | static print( 13 | content: string, 14 | colorOrOptions?: BasicFgColor | StyleOptions, 15 | options?: StyleOptions, 16 | ) { 17 | let output = content; 18 | 19 | if (colorOrOptions && typeof colorOrOptions === "object") { 20 | output = ColorMe.fromOptions(content, colorOrOptions); 21 | } 22 | if (colorOrOptions && typeof colorOrOptions === "string") { 23 | output = ColorMe.fromOptions(content, { 24 | color: colorOrOptions, 25 | ...options, 26 | }); 27 | } 28 | if (!colorOrOptions && options) { 29 | output = ColorMe.fromOptions(content, { color: "white", ...options }); 30 | } 31 | this.write(output); 32 | } 33 | static goToTop() { 34 | this.write("\x1b[H"); 35 | } 36 | static goTo(row: number, column: number) { 37 | this.write(`\x1b[${row};${column}H`); 38 | } 39 | static goToColumn(column: number) { 40 | this.write(`\x1b[${column}G`); 41 | } 42 | static clear() { 43 | this.write("\x1b[2J"); 44 | } 45 | static clearCurrentLine() { 46 | this.write("\x1b[2K"); 47 | } 48 | static hideCursor() { 49 | this.write("\x1b[?25l"); 50 | } 51 | static showCursor() { 52 | this.write("\x1b[?25h"); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/orm/orm-exception.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An exception class for ORM-related errors. 3 | */ 4 | export class ORMException extends Error { 5 | /** 6 | * The subject of the exception. 7 | */ 8 | subject?: string; 9 | /** 10 | * The HTTP response code to use. 11 | */ 12 | responseCode?: number; 13 | /** 14 | * The type of the exception. This can be "error" or "warning". 15 | * @default "error" 16 | */ 17 | type: "error" | "warning" = "error"; 18 | /** 19 | * Create a new ORMException. 20 | * @param message - The message to include in the exception. 21 | * @param subject - The subject of the exception. 22 | * @param responseCode - The HTTP response code to use. 23 | */ 24 | constructor(message: string, subject?: string, responseCode?: number) { 25 | super(message); 26 | this.subject = subject || "ORM Exception"; 27 | this.responseCode = responseCode || 500; 28 | switch (this.responseCode) { 29 | case 404: 30 | this.type = "warning"; 31 | break; 32 | } 33 | 34 | this.name = "ORMException"; 35 | } 36 | } 37 | 38 | /** 39 | * Raise an ORMException with the given message. 40 | * @param message - The message to include in the exception. 41 | * @param subject - The subject of the exception. 42 | * @param responseCode - The HTTP response code to use. 43 | */ 44 | 45 | export function raiseORMException( 46 | message: string, 47 | subject?: string, 48 | responseCode?: number, 49 | ): never { 50 | throw new ORMException(message, subject, responseCode); 51 | } 52 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | import type { ChildEntryList } from "~/orm/child-entry/child-entry.ts"; 2 | export type { InCloud } from "~/in-cloud.ts"; 3 | 4 | export type { 5 | EntryHookFunction, 6 | EntryHookName, 7 | EntryHooks, 8 | GetListResponse, 9 | GlobalEntryHooks, 10 | GlobalHookFunction, 11 | HookName, 12 | } from "~/orm/orm-types.ts"; 13 | 14 | export type { 15 | FetchOptions, 16 | InField, 17 | InFieldMap, 18 | InFieldType, 19 | IntFormat, 20 | } from "~/orm/field/field-def-types.ts"; 21 | 22 | export type { Choice, IDMode, InValue } from "~/orm/field/types.ts"; 23 | 24 | export type { EntryBase } from "~/orm/entry/entry-base.ts"; 25 | export type { SettingsBase } from "~/orm/settings/settings-base.ts"; 26 | export type { SessionData } from "~/auth/types.ts"; 27 | 28 | export type ChildList> = ChildEntryList; 29 | 30 | export type { User } from "~/auth/entries/user/_user.type.ts"; 31 | export type { UserSession } from "~/auth/entries/user-session/_user-session.type.ts"; 32 | export type { AuthSettings } from "~/auth/settings/_auth-settings.type.ts"; 33 | export type { CloudFile } from "~/files/entries/_cloud-file.type.ts"; 34 | export type { EmailSettings } from "~/email/settings/_email-settings.type.ts"; 35 | export type { Email } from "~/email/entries/_email.type.ts"; 36 | export type { EmailAccount } from "~/email/entries/_email-account.type.ts"; 37 | export type { InTaskGlobal } from "~/in-queue/entry-types/in-task/_in-task-global.type.ts"; 38 | export type { SystemSettings } from "~/extension/settings/_system-settings.type.ts"; 39 | -------------------------------------------------------------------------------- /src/serve/in-context.ts: -------------------------------------------------------------------------------- 1 | import { 2 | raiseServerException, 3 | // deno-lint-ignore no-unused-vars 4 | type ServerException, 5 | } from "~/serve/server-exception.ts"; 6 | 7 | /** 8 | * The InContext class is instantiated for each inRequest and assigned to the inRequest.context property. 9 | * It is used to store context data that is shared between lifecycle handlers, middleware, and path handlers 10 | * for the duration of the request. 11 | */ 12 | export class InContext< 13 | Context extends Record = Record, 14 | > { 15 | #context = new Map(); 16 | /** Register a new key-value pair in the context. 17 | * @throws {ServerException} if the key already exists. 18 | * This is a guard against accidentally overwriting a value that's already in the context. 19 | */ 20 | register(key: K, value: Context[K]): void { 21 | if (this.#context.has(key)) { 22 | raiseServerException(400, `Context key ${key.toString()} already exists`); 23 | } 24 | this.#context.set(key, value); 25 | } 26 | /** Get a value from the context. */ 27 | get(key: string): T | undefined { 28 | return this.#context.get(key); 29 | } 30 | 31 | /** Update a value for a given key in the context. 32 | * @throws {ServerException} If the key does not exist. 33 | */ 34 | update(key: K, value: Context[K]): void { 35 | if (!this.#context.has(key)) { 36 | raiseServerException(400, `Context key ${key.toString()} does not exist`); 37 | } 38 | this.#context.set(key, value); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/orm/migrate/entry-type/entry-migration-plan.ts: -------------------------------------------------------------------------------- 1 | import type { ForeignKeyConstraint } from "~/orm/db/db-types.ts"; 2 | import type { IDMode } from "~/orm/field/types.ts"; 3 | import type { 4 | ColumnCreatePlan, 5 | ColumnMigrationPlan, 6 | } from "~/orm/migrate/types.ts"; 7 | import type { EntryIndex } from "~/orm/entry/types.ts"; 8 | 9 | export class EntryMigrationPlan { 10 | entryType: string; 11 | table: { 12 | tableName: string; 13 | create: boolean; 14 | idMode: IDMode; 15 | updateDescription?: { 16 | from: string; 17 | to: string; 18 | }; 19 | }; 20 | columns: { 21 | create: Array; 22 | drop: Array; 23 | modify: Array; 24 | }; 25 | constraints: { 26 | foreignKey: { 27 | create: Array; 28 | drop: Array; 29 | }; 30 | }; 31 | indexes: { 32 | create: Array & { indexName: string }>; 33 | drop: Array; 34 | }; 35 | children: Array; 36 | constructor(entryType: string) { 37 | this.entryType = entryType; 38 | this.table = { 39 | tableName: "", 40 | idMode: "ulid", 41 | create: false, 42 | }; 43 | this.columns = { 44 | create: [], 45 | drop: [], 46 | modify: [], 47 | }; 48 | 49 | this.constraints = { 50 | foreignKey: { 51 | create: [], 52 | drop: [], 53 | }, 54 | }; 55 | this.children = []; 56 | this.indexes = { 57 | create: [], 58 | drop: [], 59 | }; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/onboarding/actions/complete-onboarding.ts: -------------------------------------------------------------------------------- 1 | import { CloudAPIAction } from "../../api/cloud-action.ts"; 2 | import type { Account } from "../../auth/entries/account/_account.type.ts"; 3 | import type { SessionData } from "../../auth/types.ts"; 4 | import { raiseCloudException } from "../../serve/exeption/cloud-exception.ts"; 5 | 6 | export const completeOnboarding = new CloudAPIAction("completeOnboarding", { 7 | async run({ inRequest, params: { obResponse }, inCloud, orm }) { 8 | const user = inRequest.context.get("user")!; 9 | const account = await inCloud.orm.withAccount(user.accountId) 10 | .getEntry( 11 | "account", 12 | user.accountId, 13 | ); 14 | if (account.onboardingComplete) { 15 | raiseCloudException( 16 | "Onboarding is already complete for this account.", 17 | { 18 | scope: "Onboarding", 19 | type: "warning", 20 | }, 21 | ); 22 | } 23 | const { afterOnboarding } = inCloud.extensionManager; 24 | 25 | for (const action of afterOnboarding) { 26 | await action({ 27 | account, 28 | inCloud, 29 | orm, 30 | responses: obResponse, 31 | }); 32 | } 33 | account.onboardingComplete = true; 34 | account.obResponse = obResponse; 35 | await account.save(); 36 | return { 37 | message: "Onboarding completed successfully.", 38 | accountId: account.id, 39 | }; 40 | }, 41 | params: [{ 42 | key: "obResponse", 43 | label: "Onboarding Response", 44 | type: "JSONField", 45 | required: true, 46 | }], 47 | }); 48 | -------------------------------------------------------------------------------- /src/orm/api-actions/groups.ts: -------------------------------------------------------------------------------- 1 | import { CloudAPIGroup } from "../../api/cloud-group.ts"; 2 | import { 3 | count, 4 | countConnections, 5 | createEntryAction, 6 | deleteEntryAction, 7 | getEntryAction, 8 | getEntryListAction, 9 | getEntryTypeInfoAction, 10 | newEntryAction, 11 | runEntryAction, 12 | sum, 13 | updateEntryAction, 14 | } from "./entries-group.ts"; 15 | import { 16 | entryTypesInfo, 17 | generateInterfaces, 18 | getClientInterfaces, 19 | settingsTypesInfo, 20 | } from "./orm-group.ts"; 21 | import { 22 | getSettings, 23 | getSettingsInfo, 24 | runSettingsAction, 25 | updateSettings, 26 | } from "./settings-group.ts"; 27 | 28 | export const entriesGroup = new CloudAPIGroup("entry", { 29 | description: "CRUD actions for InSpatial ORM Entries", 30 | actions: [ 31 | getEntryTypeInfoAction, 32 | getEntryAction, 33 | runEntryAction, 34 | updateEntryAction, 35 | newEntryAction, 36 | createEntryAction, 37 | deleteEntryAction, 38 | getEntryListAction, 39 | countConnections, 40 | sum, 41 | count, 42 | ], 43 | }); 44 | 45 | export const ormGroup = new CloudAPIGroup("orm", { 46 | description: "ORM related actions", 47 | label: "ORM", 48 | actions: [ 49 | entryTypesInfo, 50 | settingsTypesInfo, 51 | generateInterfaces, 52 | getClientInterfaces, 53 | ], 54 | }); 55 | 56 | export const settingsGroup = new CloudAPIGroup("settings", { 57 | description: "Actions for managing settings", 58 | actions: [ 59 | getSettingsInfo, 60 | getSettings, 61 | updateSettings, 62 | runSettingsAction, 63 | ], 64 | }); 65 | -------------------------------------------------------------------------------- /src/orm/build/generate-interface/build-fields.ts: -------------------------------------------------------------------------------- 1 | import { fieldTypeMap } from "~/orm/build/generate-interface/field-type-map.ts"; 2 | import { raiseORMException } from "~/orm/orm-exception.ts"; 3 | import type { InField } from "~/orm/field/field-def-types.ts"; 4 | 5 | export function buildField(field: InField): string { 6 | const { label, description, required } = field; 7 | let fieldType = fieldTypeMap[field.type]; 8 | if (field.type === "ChoicesField") { 9 | fieldType = field.choices?.map((choice) => { 10 | return `'${choice.key as string}'`; 11 | }).join(" | ") || "string"; 12 | } 13 | if (!fieldType) { 14 | raiseORMException( 15 | `Field type ${field.type} does not exist`, 16 | "Build", 17 | 400, 18 | ); 19 | } 20 | 21 | const lines = [ 22 | `/**`, 23 | ` * **${label || ""}** (${field.type})`, 24 | ]; 25 | if (field.type === "ConnectionField") { 26 | lines.push(" *"); 27 | lines.push(` * **EntryType** \`${field.entryType}\``); 28 | } 29 | if (description) { 30 | lines.push(` * @description ${description}`); 31 | } 32 | lines.push(` * @type {${fieldType}}`); 33 | if (required) { 34 | lines.push(` * @required ${required}`); 35 | } 36 | lines.push(` */`); 37 | 38 | lines.push( 39 | `${field.key}${required ? "" : "?"}: ${fieldType};`, 40 | ); 41 | return lines.join("\n"); 42 | } 43 | 44 | export function buildFields( 45 | fieldDefs: Map, 46 | ): Array { 47 | const fields: string[] = []; 48 | fieldDefs.forEach((field) => { 49 | fields.push(buildField(field)); 50 | }); 51 | return fields; 52 | } 53 | -------------------------------------------------------------------------------- /src/files/actions/upload-file.ts: -------------------------------------------------------------------------------- 1 | import { CloudAPIAction } from "~/api/cloud-action.ts"; 2 | import type { CloudFile } from "../entries/_cloud-file.type.ts"; 3 | import MimeTypes from "../mime-types/mime-types.ts"; 4 | 5 | export const uploadFile = new CloudAPIAction("upload", { 6 | label: "Upload File", 7 | raw: true, 8 | params: [], 9 | async run({ inCloud, orm, inRequest, inResponse }) { 10 | const formData = await inRequest.request.formData(); 11 | const file = formData.get("content") as File; 12 | const fileName = formData.get("fileName") as string; 13 | 14 | const cloudFile = orm.getNewEntry("cloudFile"); 15 | cloudFile.fileName = fileName; 16 | cloudFile.fileSize = file.size; 17 | cloudFile.mimeType = file.type as any; 18 | const extensionInfo = MimeTypes.getExtensionsByMimeType(file.type); 19 | if (extensionInfo) { 20 | cloudFile.fileType = extensionInfo.category; 21 | cloudFile.fileExtension = extensionInfo.extension as any; 22 | cloudFile.fileTypeDescription = extensionInfo.description; 23 | } 24 | cloudFile.filePath = ""; 25 | await cloudFile.save(); 26 | const id = cloudFile.id; 27 | const extension = fileName.split(".").pop(); 28 | const newFileName = `${id}.${extension}`; 29 | const stream = file.stream(); 30 | const path = `${inCloud.filesPath}/${newFileName}`; 31 | await Deno.writeFile(path, stream, { 32 | create: true, 33 | }); 34 | cloudFile.filePath = path; 35 | await cloudFile.save(); 36 | inResponse.setContent({ 37 | file: cloudFile.data, 38 | }); 39 | return inResponse.respond(); 40 | }, 41 | }); 42 | -------------------------------------------------------------------------------- /src/orm/orm-types.ts: -------------------------------------------------------------------------------- 1 | import type { Entry } from "~/orm/entry/entry.ts"; 2 | import type { InSpatialORM } from "~/orm/inspatial-orm.ts"; 3 | import type { QueryResultFormatted } from "~/orm/db/db-types.ts"; 4 | import type { InCloud } from "~/in-cloud.ts"; 5 | import type { Settings } from "./settings/settings.ts"; 6 | 7 | export type GlobalHookFunction = (hookParams: { 8 | inCloud: InCloud; 9 | entryType: string; 10 | entry: Entry; 11 | orm: InSpatialORM; 12 | }) => Promise | void; 13 | export type GlobalSettingsHookFunction = (hookParams: { 14 | inCloud: InCloud; 15 | settings: Settings; 16 | orm: InSpatialORM; 17 | }) => Promise | void; 18 | export type HookName = 19 | | "beforeValidate" 20 | | "validate" 21 | | "beforeUpdate" 22 | | "afterUpdate"; 23 | export type SettingsHookName = HookName; 24 | export type EntryHookName = 25 | | HookName 26 | | "beforeCreate" 27 | | "afterCreate" 28 | | "beforeDelete" 29 | | "afterDelete"; 30 | export type GlobalEntryHooks = Record< 31 | EntryHookName, 32 | Array 33 | >; 34 | 35 | export type GlobalSettingsHooks = Record< 36 | SettingsHookName, 37 | Array 38 | >; 39 | 40 | export type GetListResponse = QueryResultFormatted; 41 | 42 | export type EntryHooks = Record>; 43 | 44 | export type EntryHookFunction = (app: InCloud, hookParams: { 45 | inCloud: InCloud; 46 | entryType: string; 47 | entry: Entry; 48 | orm: InSpatialORM; 49 | }) => Promise | void; 50 | export type UniqueArray = T extends ReadonlyArray 51 | ? U[] & { __unique: never } 52 | : never; 53 | -------------------------------------------------------------------------------- /src/auth/entries/account/account.ts: -------------------------------------------------------------------------------- 1 | import { ChildEntryType, EntryType } from "@inspatial/cloud"; 2 | import type { Account } from "./_account.type.ts"; 3 | 4 | export const account = new EntryType("account", { 5 | label: "Account", 6 | systemGlobal: true, 7 | description: "An account in the system with one or more associated users", 8 | fields: [{ 9 | key: "onboardingComplete", 10 | type: "BooleanField", 11 | readOnly: false, 12 | }, { 13 | key: "initialized", 14 | type: "BooleanField", 15 | readOnly: false, 16 | }, { 17 | key: "obResponse", 18 | label: "Onboarding Response", 19 | type: "JSONField", 20 | }], 21 | children: [ 22 | new ChildEntryType("users", { 23 | label: "Users", 24 | fields: [{ 25 | key: "user", 26 | label: "User", 27 | type: "ConnectionField", 28 | entryType: "user", 29 | required: true, 30 | }, { 31 | key: "role", 32 | label: "Role", 33 | type: "ChoicesField", 34 | defaultValue: "accountOwner", 35 | choices: [], 36 | }], 37 | description: "Users associated with this account", 38 | }), 39 | ], 40 | }); 41 | 42 | account.addAction({ 43 | key: "initialize", 44 | label: "Initialize Account", 45 | private: true, 46 | async action({ account, inCloud }) { 47 | if (account.initialized) return; 48 | const schemaId = account.id; 49 | await inCloud.orm.db.createSchema(schemaId); 50 | await inCloud.orm.migrate(schemaId); 51 | // await account.load(account.id); 52 | account.initialized = true; 53 | await account.save(); 54 | }, 55 | params: [], 56 | }); 57 | -------------------------------------------------------------------------------- /src/auth/actions/register-user.ts: -------------------------------------------------------------------------------- 1 | import { CloudAPIAction } from "~/api/cloud-action.ts"; 2 | import { raiseCloudException } from "../../serve/exeption/cloud-exception.ts"; 3 | import type { SystemSettings } from "../../extension/settings/_system-settings.type.ts"; 4 | 5 | const registerUser = new CloudAPIAction("registerUser", { 6 | description: "Register a new user", 7 | hideFromApi: true, 8 | async run({ orm, params }) { 9 | const { enableSignup } = await orm.getSettings( 10 | "systemSettings", 11 | ); 12 | if (!enableSignup) { 13 | raiseCloudException("User signup is disabled", { 14 | type: "warning", 15 | }); 16 | } 17 | const { firstName, lastName, email, password } = params; 18 | const user = await orm.createEntry("user", { 19 | firstName, 20 | lastName, 21 | email, 22 | }); 23 | await user.runAction("setPassword", { password }); 24 | return user.data; 25 | }, 26 | params: [{ 27 | key: "firstName", 28 | label: "First Name", 29 | description: "First name of the user", 30 | type: "DataField", 31 | required: true, 32 | }, { 33 | key: "lastName", 34 | label: "Last Name", 35 | description: "Last name of the user", 36 | type: "DataField", 37 | required: true, 38 | }, { 39 | key: "email", 40 | label: "Email", 41 | description: "Email of the user", 42 | type: "EmailField", 43 | required: true, 44 | }, { 45 | key: "password", 46 | label: "Password", 47 | description: "Password of the user", 48 | type: "PasswordField", 49 | required: true, 50 | }], 51 | }); 52 | 53 | export default registerUser; 54 | -------------------------------------------------------------------------------- /src/orm/shared/shared-types.ts: -------------------------------------------------------------------------------- 1 | import type { InField } from "~/orm/field/field-def-types.ts"; 2 | import type { 3 | ChildEntryType, 4 | ChildEntryTypeInfo, 5 | } from "~/orm/child-entry/child-entry.ts"; 6 | 7 | export interface BaseTypeInfo { 8 | name: string; 9 | description: string; 10 | systemGlobal: boolean; 11 | extension?: string; 12 | label: string; 13 | fields: Array; 14 | titleFields: Array; 15 | children?: Array; 16 | displayFields: Array; 17 | fieldGroups: Array; 18 | } 19 | 20 | export interface BaseTypeConfig { 21 | label: string; 22 | description: string; 23 | extension?: { 24 | extensionType: { 25 | key: string; 26 | label: string; 27 | }; 28 | key: string; 29 | label: string; 30 | description: string; 31 | version?: string; 32 | }; 33 | } 34 | 35 | export interface BaseConfig { 36 | label?: string; 37 | description?: string; 38 | /** 39 | * If true, this will exist in the shared schema of the database that all tenants can reference. 40 | */ 41 | systemGlobal?: boolean; 42 | fields: Array; 43 | fieldGroups?: Array>; 44 | children?: Array>; 45 | dir?: string; 46 | } 47 | 48 | export interface FieldGroupConfig { 49 | key: string; 50 | label?: string; 51 | description?: string; 52 | fields: Array; 53 | } 54 | 55 | export interface FieldGroup { 56 | key: string; 57 | label: string; 58 | description?: string; 59 | fields: Array; 60 | displayFields: Array; 61 | } 62 | -------------------------------------------------------------------------------- /src/extension/orm-hooks/in-live-notify.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | GlobalHookFunction, 3 | GlobalSettingsHookFunction, 4 | } from "~/orm/orm-types.ts"; 5 | 6 | export const notifyUpdate: GlobalHookFunction = ( 7 | { inCloud: { inLive }, entry, entryType }, 8 | ) => { 9 | if (entry._db._schema === undefined) { 10 | return; 11 | } 12 | inLive.notify({ 13 | accountId: entry._db._schema!, 14 | roomName: `${entryType}:${entry.id}`, 15 | event: "update", 16 | data: entry.data, 17 | }); 18 | inLive.notify({ 19 | accountId: entry._db._schema!, 20 | roomName: entryType, 21 | event: "update", 22 | data: entry.data, 23 | }); 24 | }; 25 | 26 | export const notifyCreate: GlobalHookFunction = ( 27 | { inCloud: { inLive }, entry, entryType }, 28 | ) => { 29 | if (entry._db.schema === undefined) { 30 | return; 31 | } 32 | 33 | inLive.notify({ 34 | accountId: entry._db._schema!, 35 | roomName: entryType, 36 | event: "create", 37 | data: entry.data, 38 | }); 39 | }; 40 | 41 | export const notifyDelete: GlobalHookFunction = ( 42 | { inCloud: { inLive }, entry, entryType }, 43 | ) => { 44 | if (entry._db.schema === undefined) { 45 | return; 46 | } 47 | inLive.notify({ 48 | accountId: entry._db._schema!, 49 | roomName: entryType, 50 | event: "delete", 51 | data: entry.data, 52 | }); 53 | }; 54 | 55 | export const notifySettings: GlobalSettingsHookFunction = ( 56 | { inCloud: { inLive }, settings }, 57 | ) => { 58 | if (settings._db.schema === undefined) { 59 | return; 60 | } 61 | inLive.notify({ 62 | accountId: settings._db._schema!, 63 | roomName: `settings:${settings._name}`, 64 | event: "update", 65 | data: settings.data, 66 | }); 67 | }; 68 | -------------------------------------------------------------------------------- /src/utils/path-utils.ts: -------------------------------------------------------------------------------- 1 | export const OS = Deno.build.os; 2 | export const IS_WINDOWS = OS === "windows"; 3 | export const IS_DARWIN = OS === "darwin"; 4 | export const IS_LINUX = OS === "linux"; 5 | export const IS_UNIX = IS_DARWIN || IS_LINUX; 6 | 7 | /** 8 | * Joins multiple path segments into a single path. 9 | * The path segments are joined with a forward slash (/) regardless of the OS. 10 | */ 11 | export function joinPath(...paths: string[]): string { 12 | const path = paths.join("/"); 13 | if (IS_WINDOWS) { 14 | return path.replace(/\\/g, "/"); 15 | } 16 | return path; 17 | } 18 | 19 | export function normalizePath(path: string, options?: { 20 | /** 21 | * Whether to drop the file name at the end of the path and return only the directory name. 22 | */ 23 | toDirname?: boolean; 24 | }): string { 25 | path = path.replace("/^file:\/\//", ""); 26 | path = decodeURIComponent(path); 27 | path = IS_WINDOWS 28 | ? path.replace(/\\/g, "/").replace(/^\/([A-Z]:)/, "$1") 29 | : path; 30 | if (options?.toDirname) { 31 | path = path.replace(/\/[\w\s]*\.\w*$/, ""); 32 | } 33 | return path; 34 | } 35 | 36 | /** Returns the folder path of the calling function where this `getCallerPath()` function is called. 37 | * ONLY if the module is a local file url (`file:///...`) 38 | */ 39 | export function getCallerPath(): string | undefined { 40 | const matchPattern = /(file:\/\/.+)\/[\w-_\s\d]+.ts:\d+:\d+/; 41 | const callingFunction = new Error().stack?.split("\n")[3]; 42 | const match = callingFunction?.match(matchPattern); 43 | if (match) { 44 | const dir = new URL(match[1]); 45 | if (dir.protocol === "file:") { 46 | return normalizePath(dir.pathname); 47 | } 48 | } 49 | return undefined; 50 | } 51 | -------------------------------------------------------------------------------- /src/auth/entries/user/fields/fields.ts: -------------------------------------------------------------------------------- 1 | import type { InField } from "~/orm/field/field-def-types.ts"; 2 | 3 | export const userFields = [{ 4 | key: "firstName", 5 | type: "DataField", 6 | label: "First Name", 7 | description: "The user's first name", 8 | required: true, 9 | }, { 10 | key: "lastName", 11 | type: "DataField", 12 | label: "Last Name", 13 | description: "The user's last names", 14 | required: true, 15 | }, { 16 | key: "email", 17 | type: "EmailField", 18 | label: "Email", 19 | description: "The user's email address used for login", 20 | required: true, 21 | unique: true, 22 | }, { 23 | key: "fullName", 24 | type: "DataField", 25 | label: "Full Name", 26 | description: "The user's full name (automatically generated)", 27 | readOnly: true, 28 | }, { 29 | key: "profilePicture", 30 | type: "ImageField", 31 | label: "Profile Picture", 32 | allowedImageTypes: ["png", "jpeg", "svg+xml", "png"], 33 | description: "The user's profile picture", 34 | }, { 35 | key: "password", 36 | type: "PasswordField", 37 | label: "Password", 38 | hidden: true, 39 | description: "The user's password used for login", 40 | }, { 41 | key: "resetPasswordToken", 42 | label: "Reset Password Token", 43 | type: "PasswordField", 44 | description: "The token used to reset the user's password", 45 | readOnly: true, 46 | hidden: true, 47 | }, { 48 | key: "systemAdmin", 49 | label: "System Administrator", 50 | type: "BooleanField", 51 | readOnly: false, 52 | description: 53 | "Is the user a system administrator? (admin users have access to all parts of the system)", 54 | }, { 55 | key: "apiToken", 56 | label: "API Token", 57 | type: "PasswordField", 58 | description: "The user's API token", 59 | readOnly: true, 60 | }] as Array; 61 | -------------------------------------------------------------------------------- /src/orm/entry/build-entry.ts: -------------------------------------------------------------------------------- 1 | import type { EntryType } from "~/orm/entry/entry-type.ts"; 2 | 3 | import { Entry } from "~/orm/entry/entry.ts"; 4 | import type { InSpatialORM } from "~/orm/inspatial-orm.ts"; 5 | import type { EntryActionDefinition } from "~/orm/entry/types.ts"; 6 | import { makeFields } from "~/orm/build/make-fields.ts"; 7 | import { buildChildren } from "~/orm/child-entry/build-children.ts"; 8 | import type { InField } from "~/orm/field/field-def-types.ts"; 9 | import type { InCloud } from "~/in-cloud.ts"; 10 | import type { UserID } from "~/auth/types.ts"; 11 | 12 | export function buildEntry(entryType: EntryType): typeof Entry { 13 | const changeableFields = new Map(); 14 | for (const field of entryType.fields.values()) { 15 | if ( 16 | !field.readOnly && !field.hidden && 17 | !["id", "updatedAt", "changedAt"].includes(field.key) 18 | ) { 19 | changeableFields.set(field.key, field); 20 | } 21 | } 22 | const childrenClasses = buildChildren(entryType); 23 | const entryClass = class extends Entry { 24 | override _fields: Map = entryType.fields; 25 | override _changeableFields = changeableFields; 26 | override _titleFields: Map = 27 | entryType.connectionTitleFields; 28 | override _actions: Map = entryType.actions; 29 | override _childrenClasses = childrenClasses; 30 | override readonly _entryType = entryType; 31 | 32 | constructor(config: { orm: InSpatialORM; inCloud: InCloud; user: UserID }) { 33 | super({ 34 | systemGlobal: entryType.systemGlobal, 35 | orm: config.orm, 36 | inCloud: config.inCloud, 37 | name: entryType.name, 38 | user: config.user, 39 | }); 40 | this._setupChildren(); 41 | } 42 | }; 43 | 44 | makeFields("entry", entryType, entryClass); 45 | 46 | return entryClass; 47 | } 48 | -------------------------------------------------------------------------------- /src/cloud-config/config-types.ts: -------------------------------------------------------------------------------- 1 | import type { CoreConfig } from "../extension/core-config.ts"; 2 | 3 | /** 4 | * The configuration for an environment variable. 5 | */ 6 | export type ConfigEnv< 7 | T extends keyof ConfigEnvTypeMap = keyof ConfigEnvTypeMap, 8 | > = { 9 | env?: string; 10 | description: string; 11 | required?: boolean; 12 | dependsOn?: 13 | | { 14 | key: string; 15 | value: ConfigEnvTypeMap[T]; 16 | } 17 | | Array<{ 18 | key: string; 19 | value: ConfigEnvTypeMap[T]; 20 | }>; 21 | default?: ConfigEnvTypeMap[T]; 22 | enum?: ConfigEnvTypeMap[T][]; 23 | type: T; 24 | }; 25 | 26 | /** 27 | * The type map for the ConfigEnv type definition. 28 | */ 29 | export interface ConfigEnvTypeMap { 30 | /** 31 | * A 'string' type. 32 | */ 33 | string: string; 34 | 35 | /** 36 | * A 'number' type. 37 | */ 38 | number: number; 39 | 40 | /** 41 | * A 'boolean' type. 42 | */ 43 | boolean: boolean; 44 | 45 | /** 46 | * An array of 'string' type. 47 | */ 48 | "string[]": Array; 49 | } 50 | 51 | /** 52 | * Extract the value type from a ConfigEnv definition. 53 | */ 54 | export type ExtractConfigEnvValue = C extends 55 | ConfigEnv ? ConfigEnvTypeMap[T] 56 | : never; 57 | 58 | /** 59 | * The configuration values for the extension. 60 | */ 61 | export type ExtensionConfig = C extends 62 | ConfigDefinition ? { 63 | [P in K]: ExtractConfigEnvValue; 64 | } 65 | : never; 66 | 67 | export type ExtractConfig = ConfigMap[K]; 68 | 69 | export type ConfigKey = keyof ConfigMap; 70 | 71 | export interface ConfigMap { 72 | core: CoreConfig; 73 | } 74 | 75 | /** 76 | * The definition configuration of the environment variables for the extension. 77 | */ 78 | export type ConfigDefinition = Record; 79 | -------------------------------------------------------------------------------- /src/orm/child-entry/build-children.ts: -------------------------------------------------------------------------------- 1 | import type { EntryType } from "~/orm/entry/entry-type.ts"; 2 | import type { SettingsType } from "~/orm/settings/settings-type.ts"; 3 | import { ChildEntry, ChildEntryList } from "~/orm/child-entry/child-entry.ts"; 4 | import { makeFields } from "~/orm/build/make-fields.ts"; 5 | import type { InSpatialORM } from "~/orm/inspatial-orm.ts"; 6 | import type { InField } from "~/orm/field/field-def-types.ts"; 7 | import type { InSpatialDB } from "../db/inspatial-db.ts"; 8 | 9 | export function buildChildren( 10 | entryOrSettingsType: EntryType | SettingsType, 11 | ): Map { 12 | const childrenClasses = new Map(); 13 | if (entryOrSettingsType.children) { 14 | for (const child of entryOrSettingsType.children.values()) { 15 | const changeableFields = new Map(); 16 | for (const field of child.fields.values()) { 17 | if ( 18 | !field.readOnly && !field.hidden && 19 | !["id", "updatedAt", "changedAt"].includes(field.key) 20 | ) { 21 | changeableFields.set(field.key, field); 22 | } 23 | } 24 | const childClass = class extends ChildEntry {}; 25 | makeFields("child", child, childClass); 26 | 27 | const childListClass = class extends ChildEntryList { 28 | override _name = child.name; 29 | override _childClass = childClass; 30 | override _fields = child.fields; 31 | override _tableName = child.config.tableName!; 32 | override _changeableFields = changeableFields; 33 | override _titleFields: Map = 34 | child.connectionTitleFields; 35 | 36 | constructor(orm: InSpatialORM, db: InSpatialDB) { 37 | super(orm, db); 38 | } 39 | }; 40 | 41 | childrenClasses.set(child.name, childListClass); 42 | } 43 | } 44 | return childrenClasses; 45 | } 46 | -------------------------------------------------------------------------------- /src/orm/field/fields.ts: -------------------------------------------------------------------------------- 1 | import textField from "~/orm/field/fields/text-field.ts"; 2 | import bigIntField from "~/orm/field/fields/big-int-field.ts"; 3 | import booleanField from "~/orm/field/fields/boolean-field.ts"; 4 | import choicesField from "~/orm/field/fields/choices-field.ts"; 5 | import connectionField from "~/orm/field/fields/connection-field.ts"; 6 | import currencyField from "~/orm/field/fields/currency-field.ts"; 7 | import dateField from "~/orm/field/fields/date-field.ts"; 8 | import { dataField } from "~/orm/field/fields/data-field.ts"; 9 | import decimalField from "~/orm/field/fields/decimal-field.ts"; 10 | import emailField from "~/orm/field/fields/email-field.ts"; 11 | import imageField from "~/orm/field/fields/image-field.ts"; 12 | import intField from "~/orm/field/fields/int-field.ts"; 13 | import jsonField from "~/orm/field/fields/json-field.ts"; 14 | import listField from "~/orm/field/fields/list-field.ts"; 15 | import multiChoiceField from "~/orm/field/fields/multi-choice-field.ts"; 16 | import passwordField from "~/orm/field/fields/password-field.ts"; 17 | import phoneField from "~/orm/field/fields/phone-field.ts"; 18 | import richTextField from "~/orm/field/fields/rich-text-field.ts"; 19 | import timestampField from "~/orm/field/fields/timestamp-field.ts"; 20 | import urlField from "~/orm/field/fields/url-field.ts"; 21 | import idField from "~/orm/field/fields/id-field.ts"; 22 | import fileField from "~/orm/field/fields/file-field.ts"; 23 | import timeField from "./fields/time-field.ts"; 24 | 25 | export const ormFields = [ 26 | bigIntField, 27 | booleanField, 28 | choicesField, 29 | connectionField, 30 | currencyField, 31 | dataField, 32 | dateField, 33 | decimalField, 34 | emailField, 35 | imageField, 36 | intField, 37 | jsonField, 38 | listField, 39 | multiChoiceField, 40 | passwordField, 41 | phoneField, 42 | richTextField, 43 | textField, 44 | timestampField, 45 | urlField, 46 | idField, 47 | fileField, 48 | timeField, 49 | ]; 50 | -------------------------------------------------------------------------------- /src/in-log/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The log levels 3 | */ 4 | export type LogType = "error" | "info" | "warning" | "debug" | "message"; 5 | 6 | /** 7 | * The log levels 8 | */ 9 | export type LogLevel = "error" | "info" | "warning" | "debug"; 10 | /** 11 | * A parsed stack frame 12 | */ 13 | export interface StackFrame { 14 | /** 15 | * The class of the caller 16 | */ 17 | class: string | null; 18 | /** 19 | * The method of the caller 20 | */ 21 | method: string | null; 22 | /** 23 | * The path of the file 24 | */ 25 | path: string; 26 | /** 27 | * The name of the file 28 | */ 29 | fileName: string; 30 | /** 31 | * The line number of the caller 32 | */ 33 | line: string; 34 | /** 35 | * The column number of the caller 36 | */ 37 | column: string; 38 | } 39 | 40 | /** 41 | * A log message 42 | */ 43 | 44 | export interface LogMessage { 45 | /** 46 | * The content of the log message 47 | */ 48 | content: any[]; 49 | /** 50 | * The type of log message 51 | */ 52 | type: LogType; 53 | /** 54 | * The subject of the log message 55 | */ 56 | subject: string; 57 | /** 58 | * The stack frame of the caller 59 | */ 60 | caller: StackFrame | Array; 61 | /** 62 | * The timestamp of the log message 63 | */ 64 | timestamp: Date; 65 | compact?: boolean; 66 | } 67 | 68 | /** 69 | * Options for logging 70 | */ 71 | export interface LogOptions { 72 | /** 73 | * The subject of the log message 74 | */ 75 | subject?: string; 76 | stackTrace?: any; 77 | compact?: boolean; 78 | } 79 | 80 | /** 81 | * The configuration for the logger 82 | */ 83 | export interface LoggerConfig { 84 | /** 85 | * The name of the logger 86 | */ 87 | name?: string; 88 | /** 89 | * The offset to use when isolating the stack trace call frame 90 | */ 91 | traceOffset?: number; 92 | /** 93 | * The default style for console logging 94 | */ 95 | consoleDefaultStyle: "compact" | "full"; 96 | } 97 | -------------------------------------------------------------------------------- /src/orm/db/postgres/in-pg/cloud-db.ts: -------------------------------------------------------------------------------- 1 | import { InPG } from "./in-pg.ts"; 2 | 3 | export class CloudDB { 4 | inPg: InPG; 5 | 6 | constructor(pgDataRoot: string) { 7 | this.inPg = new InPG({ 8 | env: { 9 | PGDATA: pgDataRoot, 10 | MODE: "REACT", 11 | PGDATABASE: "template1", 12 | PGUSER: "postgres", 13 | PREFIX: `/tmp/pglite`, 14 | REPL: "N", 15 | }, 16 | onStderr: (out) => { 17 | console.log(out.message); 18 | }, 19 | onStdout: (out) => { 20 | console.log(out.message); 21 | }, 22 | debug: false, 23 | args: [ 24 | "--single", 25 | "postgres", 26 | "--", 27 | `PGDATA=${pgDataRoot}`, 28 | `PREFIX=/tmp/pglite`, 29 | "PGDATABASE=template1", 30 | `PGUSER=postgres`, 31 | "REPL=N", 32 | ], 33 | }); 34 | } 35 | 36 | async start( 37 | port: number, 38 | statusCallback?: (status: "starting" | "running") => void, 39 | ) { 40 | if (statusCallback) { 41 | statusCallback("starting"); 42 | } 43 | await this.inPg.run(); 44 | Deno.serve({ 45 | hostname: "127.0.0.1", 46 | port, 47 | onListen: (_addr) => { 48 | if (statusCallback) { 49 | statusCallback("running"); 50 | } 51 | }, 52 | }, (request) => { 53 | const { response, socket } = Deno.upgradeWebSocket(request); 54 | socket.addEventListener("open", () => { 55 | }); 56 | socket.addEventListener( 57 | "message", 58 | (event: MessageEvent) => { 59 | const response = this.inPg.sendQuery(new Uint8Array(event.data)); 60 | socket.send(response); 61 | }, 62 | ); 63 | socket.addEventListener("close", () => { 64 | // no-op 65 | }); 66 | socket.addEventListener("error", (_error) => { 67 | // should add error handling here? 68 | }); 69 | return response; 70 | }); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/auth/migrate/init-admin-account.ts: -------------------------------------------------------------------------------- 1 | import type { InSpatialORM } from "~/orm/inspatial-orm.ts"; 2 | import type { User } from "../entries/user/_user.type.ts"; 3 | import { inLog } from "#inLog"; 4 | import { center } from "../../terminal/format-utils.ts"; 5 | import type { Account } from "../entries/account/_account.type.ts"; 6 | 7 | export async function initAdminAccount( 8 | orm: InSpatialORM, 9 | ) { 10 | const accounts = await orm.count("account"); 11 | if (accounts > 0) { 12 | return; 13 | } 14 | const newAdminUser = await createAdminUser(orm); 15 | if (!newAdminUser) { 16 | return; 17 | } 18 | const account = await orm.createEntry("account", { 19 | users: [{ user: newAdminUser.id }], 20 | }); 21 | await account.runAction("initialize"); 22 | inLog.info("Admin account created successfully."); 23 | } 24 | 25 | async function createAdminUser(orm: InSpatialORM): Promise { 26 | // return; 27 | const userCount = await orm.count("user"); 28 | 29 | const subject = "System Admin User"; 30 | if (userCount > 0) { 31 | return; 32 | } 33 | const firstName = "InSpatial"; 34 | const lastName = "Admin"; 35 | const email = "admin@user.com"; 36 | const password = "password"; 37 | const role = "systemAdmin"; 38 | 39 | const info = [ 40 | `Creating a new admin user with the following details:`, 41 | `First Name: ${firstName}`, 42 | `Last Name: ${lastName}`, 43 | `Email: ${email}`, 44 | `Password: ${password}`, 45 | `Role: ${role}`, 46 | ]; 47 | inLog.warn( 48 | info.map((line) => center(line)).join("\n"), 49 | subject, 50 | ); 51 | 52 | const user = orm.getNewEntry("user"); 53 | user.update({ 54 | firstName, 55 | lastName, 56 | email, 57 | role, 58 | systemAdmin: true, 59 | }); 60 | 61 | user.systemAdmin = true; 62 | await user.save(); 63 | await user.runAction("setPassword", { password }); 64 | inLog.info("Admin user created successfully."); 65 | return user; 66 | } 67 | -------------------------------------------------------------------------------- /src/orm/settings/build-settings.ts: -------------------------------------------------------------------------------- 1 | import type { SettingsType } from "~/orm/settings/settings-type.ts"; 2 | import { Settings } from "~/orm/settings/settings.ts"; 3 | import type { InField } from "~/orm/field/field-def-types.ts"; 4 | import { makeFields } from "~/orm/build/make-fields.ts"; 5 | import type { SettingsActionDefinition } from "~/orm/settings/types.ts"; 6 | import { buildChildren } from "~/orm/child-entry/build-children.ts"; 7 | import type { InCloud } from "~/in-cloud.ts"; 8 | import type { UserID } from "~/auth/types.ts"; 9 | 10 | export function buildSettings( 11 | settingsType: SettingsType, 12 | ): typeof Settings { 13 | const changeableFields = new Map(); 14 | const fieldIds = new Map(); 15 | for (const field of settingsType.fields.values()) { 16 | fieldIds.set(field.key, `${settingsType.name}:${field.key}`); 17 | if ( 18 | !field.readOnly && !field.hidden 19 | ) { 20 | changeableFields.set(field.key, field); 21 | } 22 | } 23 | const childrenClasses = buildChildren(settingsType); 24 | const settingsClass = class extends Settings { 25 | override _systemGlobal: boolean = settingsType.systemGlobal; 26 | override _fields: Map = settingsType.fields; 27 | override _changeableFields = changeableFields; 28 | override _fieldIds: Map = fieldIds; 29 | override _actions: Map = 30 | settingsType.actions; 31 | override readonly _settingsType = settingsType; 32 | override _childrenClasses = childrenClasses; 33 | constructor(config: { orm: any; inCloud: InCloud; user: UserID }) { 34 | super({ 35 | systemGlobal: settingsType.systemGlobal, 36 | orm: config.orm, 37 | inCloud: config.inCloud, 38 | name: settingsType.name, 39 | user: config.user, 40 | }); 41 | this._setupChildren(); 42 | } 43 | }; 44 | 45 | makeFields("settings", settingsType, settingsClass); 46 | return settingsClass; 47 | } 48 | -------------------------------------------------------------------------------- /src/orm/registry/connection-registry.ts: -------------------------------------------------------------------------------- 1 | type EntryTypeKey = string; 2 | type FieldKey = string; 3 | 4 | export type EntryTypeRegistry = Map>; 5 | 6 | interface RegisterFieldConfig { 7 | referencingEntryType: EntryTypeKey; 8 | referencingFieldKey: FieldKey; 9 | referencingIdFieldKey: FieldKey; 10 | referencedEntryType: EntryTypeKey; 11 | referencedFieldKey: FieldKey; 12 | } 13 | 14 | export interface RegistryField { 15 | targetEntryType: EntryTypeKey; 16 | targetIdField: FieldKey; 17 | targetValueField: FieldKey; 18 | } 19 | 20 | type Registry = Map; 21 | 22 | export class ConnectionRegistry { 23 | #registry: Registry; 24 | constructor() { 25 | this.#registry = new Map(); 26 | } 27 | 28 | registerField(config: RegisterFieldConfig): void { 29 | const registryFields = this.#ensureField( 30 | config.referencedEntryType, 31 | config.referencedFieldKey, 32 | ); 33 | registryFields.push({ 34 | targetEntryType: config.referencingEntryType, 35 | targetIdField: config.referencingIdFieldKey, 36 | targetValueField: config.referencingFieldKey, 37 | }); 38 | } 39 | 40 | #ensureEntryType(entryType: EntryTypeKey): EntryTypeRegistry { 41 | if (!this.#registry.has(entryType)) { 42 | const entryTypeRegistry: EntryTypeRegistry = new Map(); 43 | this.#registry.set(entryType, entryTypeRegistry); 44 | } 45 | return this.#registry.get(entryType)!; 46 | } 47 | #ensureField( 48 | entryType: EntryTypeKey, 49 | fieldKey: FieldKey, 50 | ): Array { 51 | const entryTypeRegistry = this.#ensureEntryType(entryType); 52 | if (!entryTypeRegistry.has(fieldKey)) { 53 | const registryFields: Array = []; 54 | entryTypeRegistry.set(fieldKey, registryFields); 55 | } 56 | return entryTypeRegistry.get(fieldKey)!; 57 | } 58 | 59 | getEntryTypeRegistry(entryType: EntryTypeKey): EntryTypeRegistry | undefined { 60 | return this.#registry.get(entryType); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/auth/actions/reset-password.ts: -------------------------------------------------------------------------------- 1 | import { CloudAPIAction } from "~/api/cloud-action.ts"; 2 | 3 | import { raiseServerException } from "~/serve/server-exception.ts"; 4 | 5 | export const resetPassword = new CloudAPIAction("resetPassword", { 6 | description: "Reset user password", 7 | authRequired: false, 8 | label: "Reset Password", 9 | async run({ inCloud, orm, inRequest, params }) { 10 | const { email } = params; 11 | const user = await orm.findEntry("user", [{ 12 | field: "email", 13 | op: "=", 14 | value: email, 15 | }]); 16 | if (!user) { 17 | raiseServerException( 18 | 404, 19 | `Oops! It seems like there is no user with the email ${email}`, 20 | ); 21 | } 22 | await user.runAction("generateResetToken"); 23 | const token = user.resetPasswordToken as string; 24 | const resetLink = `${inRequest.origin}/admin/reset-password?token=${token}`; 25 | 26 | const emailContent = ` 27 |

Hi ${user.firstName},

28 |

We received a request to reset your password.

29 |

Click the link below to reset your password:

30 | ${resetLink} 31 |

If you didn't request a password reset, you can ignore this email.

32 |

Thanks!

33 |

${inCloud.appDisplayName}

34 | `; 35 | try { 36 | await inCloud.emailManager.sendEmail({ 37 | recipientEmail: email, 38 | subject: "Reset your password", 39 | body: emailContent, 40 | }); 41 | return { message: "Password reset link has been sent to your email" }; 42 | } catch (_e) { 43 | console.error("Failed to send reset password email:", _e); 44 | raiseServerException( 45 | 500, 46 | "Failed to send email", 47 | ); 48 | } 49 | }, 50 | params: [ 51 | { 52 | key: "email", 53 | label: "Email", 54 | description: "Email of the user to reset password", 55 | type: "EmailField", 56 | required: true, 57 | }, 58 | ], 59 | }); 60 | -------------------------------------------------------------------------------- /src/auth/actions/google/google-token-login.ts: -------------------------------------------------------------------------------- 1 | import { CloudAPIAction } from "~/api/cloud-action.ts"; 2 | 3 | import { GoogleOAuth } from "~/auth/providers/google/accessToken.ts"; 4 | import { raiseServerException } from "~/serve/server-exception.ts"; 5 | import type { User } from "~/auth/entries/user/_user.type.ts"; 6 | 7 | const googleTokenLogin = new CloudAPIAction("googleTokenLogin", { 8 | authRequired: false, 9 | async run({ inCloud, orm, inRequest, inResponse, params }) { 10 | const { accessToken } = params; 11 | const authSettings = await orm.getSettings("authSettings"); 12 | const googleAuth = new GoogleOAuth({ 13 | clientId: authSettings.googleClientId, 14 | clientSecret: authSettings.googleClientSecret, 15 | }); 16 | const userInfo = await googleAuth.getUserInfo(accessToken); 17 | if (!userInfo) { 18 | raiseServerException( 19 | 403, 20 | "Google auth: Failed to get user info", 21 | ); 22 | } 23 | const { email, emailVerified, picture, id } = userInfo; 24 | if (!emailVerified) { 25 | raiseServerException( 26 | 403, 27 | "Google auth: Email not verified", 28 | ); 29 | } 30 | const user = await orm.findEntry("user", [{ 31 | field: "email", 32 | op: "=", 33 | value: email, 34 | }]); 35 | 36 | if (!user) { 37 | raiseServerException( 38 | 403, 39 | "Google auth: User not found", 40 | ); 41 | } 42 | user.googleAccessToken = accessToken; 43 | user.googlePicture = picture; 44 | user.googleId = id; 45 | await user.save(); 46 | 47 | const authHandler = inCloud.auth; 48 | const sessionData = await authHandler.createUserSession( 49 | user, 50 | inRequest, 51 | inResponse, 52 | ); 53 | return sessionData; 54 | }, 55 | params: [{ 56 | key: "accessToken", 57 | type: "TextField", 58 | description: "Access token from Google OAuth2", 59 | label: "Access Token", 60 | required: true, 61 | }], 62 | }); 63 | 64 | export default googleTokenLogin; 65 | -------------------------------------------------------------------------------- /src/orm/build/generate-interface/generate-client-interface.ts: -------------------------------------------------------------------------------- 1 | import convertString from "../../../utils/convert-string.ts"; 2 | import type { EntryType } from "../../entry/entry-type.ts"; 3 | import { buildFields } from "./build-fields.ts"; 4 | 5 | export function generateClientEntryTypes(entryTypes: Array) { 6 | const generatedEntries: string[] = []; 7 | const entryTypeNames = new Map(); 8 | for (const entryType of entryTypes) { 9 | entryTypeNames.set( 10 | entryType.name, 11 | convertString(entryType.name, "pascal", true), 12 | ); 13 | const entryInterface = generateClientEntryInterface(entryType); 14 | entryInterface && generatedEntries.push(entryInterface); 15 | } 16 | const outLines: string[] = [ 17 | ...generatedEntries, 18 | "", 19 | "export type EntryMap = {", 20 | ...Array.from(entryTypeNames.entries()).map( 21 | ([name, className]) => ` ${name}: ${className}`, 22 | ), 23 | "}", 24 | "", 25 | "export type EntryName = keyof EntryMap;", 26 | "export type Entry = EntryMap[E];", 27 | ]; 28 | return [ 29 | "// This file is generated by the InSpatial Cloud Backend.", 30 | "// Do not edit this file directly.", 31 | "", 32 | ...outLines, 33 | ].join("\n"); 34 | } 35 | export function generateClientEntryInterface( 36 | entryType: EntryType, 37 | ): string | undefined { 38 | // if (!entryType.dir) { 39 | // return; 40 | // } 41 | const className = convertString(entryType.name, "pascal", true); 42 | const outLines: string[] = [ 43 | `export interface ${className} {`, 44 | ` _name:"${convertString(entryType.name, "camel", true)}"`, 45 | ]; 46 | 47 | const fields = buildFields(entryType.fields); 48 | outLines.push(...fields); 49 | for (const child of entryType.children?.values() || []) { 50 | const childFields = buildFields(child.fields); 51 | outLines.push( 52 | `${child.name}: Array<{ ${childFields.join("\n")}}>`, 53 | ); 54 | } 55 | outLines.push("}"); 56 | return outLines.join("\n"); 57 | } 58 | -------------------------------------------------------------------------------- /src/api/api-types.ts: -------------------------------------------------------------------------------- 1 | import type { InField } from "~/orm/field/field-def-types.ts"; 2 | import type { InValue } from "~/orm/field/types.ts"; 3 | 4 | /** 5 | * The full documentation for an ActionsAPI instance in JSON format. 6 | */ 7 | export interface CloudAPIDocs extends Record { 8 | /** 9 | * An array of groups in the API. 10 | * Each group contains an array of actions. 11 | */ 12 | groups: CloudAPIGroupDocs[]; 13 | } 14 | 15 | /** 16 | * The documentation for an ActionsAPI group in JSON format. 17 | */ 18 | export interface CloudAPIGroupDocs { 19 | /** 20 | * The name of the group. 21 | */ 22 | groupName: string; 23 | 24 | /** 25 | * A description of the group. 26 | */ 27 | description: string; 28 | /** 29 | * An array of actions in the group. 30 | */ 31 | 32 | /** 33 | * A label for the group to display in the UI. 34 | */ 35 | label?: string; 36 | actions: CloudAPIActionDocs[]; 37 | } 38 | 39 | /** 40 | * The documentation for an ActionsAPI action in JSON format. 41 | */ 42 | export interface CloudAPIActionDocs { 43 | /** 44 | * The name of the action. 45 | */ 46 | actionName: string; 47 | /** 48 | * A description of the action. 49 | */ 50 | description: string; 51 | /** 52 | * An array of parameters for the action. 53 | */ 54 | 55 | /** 56 | * A label for the action to display in the UI. 57 | */ 58 | label?: string; 59 | params?: Array; 60 | } 61 | 62 | /** 63 | * A typed map of parameters passed to an action handler. 64 | */ 65 | export type CloudParam

= Omit & { 66 | key: P; 67 | }; 68 | /** 69 | * A typed map of required parameters passed to an action handler. 70 | */ 71 | 72 | export type ExtractParams< 73 | K extends PropertyKey, 74 | P extends Array>, 75 | > = 76 | & { 77 | [S in P[number] as S["required"] extends true ? S["key"] : never]: InValue< 78 | S["type"] 79 | >; 80 | } 81 | & { 82 | [S in P[number] as S["required"] extends true ? never : S["key"]]?: InValue< 83 | S["type"] 84 | >; 85 | }; 86 | -------------------------------------------------------------------------------- /src/api/cloud-group.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type ActionConfig, 3 | type ActionMethod, 4 | CloudAPIAction, 5 | } from "~/api/cloud-action.ts"; 6 | import type { CloudParam } from "~/api/api-types.ts"; 7 | import { raiseServerException } from "~/serve/server-exception.ts"; 8 | 9 | export class CloudAPIGroup< 10 | G extends string = string, 11 | > { 12 | groupName: G; 13 | description: string; 14 | label?: string; 15 | actions: Map; 16 | 17 | constructor(groupName: G, config: { 18 | description: string; 19 | label?: string; 20 | actions?: Array; 21 | }) { 22 | this.groupName = groupName; 23 | this.description = config.description; 24 | this.label = config.label; 25 | this.actions = new Map(); 26 | config.actions?.forEach((action) => this.addAction(action)); 27 | } 28 | #addAction(action: CloudAPIAction) { 29 | if (this.actions.has(action.actionName)) { 30 | raiseServerException( 31 | 400, 32 | `Action name ${action.actionName} is already a registered action!`, 33 | ); 34 | } 35 | this.actions.set(action.actionName, action); 36 | } 37 | addAction< 38 | K extends PropertyKey = PropertyKey, 39 | P extends Array> = Array>, 40 | R extends ActionMethod = ActionMethod, 41 | >(name: string, config: ActionConfig): void; 42 | addAction(action: CloudAPIAction): void; 43 | addAction( 44 | nameOrAction: string | CloudAPIAction, 45 | actionConfig?: ActionConfig, 46 | ): void { 47 | if (nameOrAction instanceof CloudAPIAction) { 48 | this.#addAction(nameOrAction); 49 | return; 50 | } 51 | if (typeof nameOrAction !== "string") { 52 | raiseServerException(400, "action name must be a string"); 53 | } 54 | if (actionConfig === undefined) { 55 | raiseServerException( 56 | 400, 57 | `Please provide config options for ${nameOrAction} action`, 58 | ); 59 | } 60 | 61 | const action = new CloudAPIAction(nameOrAction, actionConfig); 62 | this.#addAction(action); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/auth/actions/google/signup-google.ts: -------------------------------------------------------------------------------- 1 | import { CloudAPIAction } from "@inspatial/cloud"; 2 | import { raiseServerException } from "../../../serve/server-exception.ts"; 3 | import { generateId } from "../../../utils/misc.ts"; 4 | import type { AuthSettings } from "../../settings/_auth-settings.type.ts"; 5 | 6 | export const signupWithGoogle = new CloudAPIAction("signupWithGoogle", { 7 | authRequired: false, 8 | description: "Sign up with Google", 9 | async run({ orm, inRequest, params }) { 10 | const { csrfToken, redirectTo } = params; 11 | const state = JSON.stringify({ 12 | redirectTo, 13 | csrfToken, 14 | type: "signup", 15 | }); 16 | const authSettings = await orm.getSettings( 17 | "authSettings", 18 | ); 19 | const clientId = authSettings.googleClientId; 20 | if (!clientId) { 21 | raiseServerException( 22 | 400, 23 | "Google auth: Client ID not set in settings", 24 | ); 25 | } 26 | const redirectUri = `${ 27 | authSettings.hostname || inRequest.fullHost 28 | }/api?group=auth&action=googleAuthCallback`; 29 | 30 | const url = new URL( 31 | "https://accounts.google.com/o/oauth2/v2/auth", 32 | ); 33 | 34 | url.searchParams.set("client_id", clientId); 35 | url.searchParams.set("redirect_uri", redirectUri); 36 | url.searchParams.set("response_type", "code"); 37 | url.searchParams.set("scope", "openid email profile"); 38 | url.searchParams.set("prompt", "consent"); 39 | const nonce = generateId(30); 40 | url.searchParams.set("nonce", nonce); 41 | url.searchParams.set("state", state); 42 | return { 43 | redirect: url.toString(), 44 | }; 45 | }, 46 | params: [{ 47 | key: "redirectTo", 48 | label: "Redirect To", 49 | type: "TextField", 50 | required: false, 51 | description: "The redirect URI to use for the Google OAuth2 login", 52 | }, { 53 | key: "csrfToken", 54 | label: "CSRF Token", 55 | type: "DataField", 56 | required: false, 57 | description: "The CSRF token to use for the Google OAuth2 login", 58 | }], 59 | }); 60 | -------------------------------------------------------------------------------- /src/serve/server-exception.ts: -------------------------------------------------------------------------------- 1 | export class Redirect { 2 | url: string; 3 | status: number; 4 | constructor(url: string, status: number = 302) { 5 | this.url = url; 6 | this.status = status; 7 | } 8 | } 9 | 10 | /** 11 | * Custom exception class for InSpatialServer server exceptions 12 | */ 13 | export class ServerException extends Error { 14 | /** 15 | * Create a new {@link ServerException} 16 | * @param message The message to include in the exception 17 | * @param status The http status code to raise 18 | */ 19 | constructor( 20 | message: string, 21 | public status: number, 22 | override name = "ServerException", 23 | ) { 24 | super(message); 25 | } 26 | } 27 | 28 | /** 29 | * Type guard for {@link ServerException} 30 | * 31 | * @param error - The error to check 32 | * @returns `true` if the error is a {@link ServerException} 33 | * 34 | * @example 35 | * ```ts 36 | * try{ 37 | * // some code that might throw an exception 38 | * } catch(error){ 39 | * if(isServerException(error)){ 40 | * // handle the exception 41 | * return; 42 | * } 43 | * throw error; 44 | * } 45 | * 46 | * ``` 47 | */ 48 | export function isServerException(error: unknown): error is ServerException { 49 | return error instanceof ServerException; 50 | } 51 | 52 | /** 53 | * Helper function to raise a {@link ServerException} 54 | * @param status The http status code to raise 55 | * @param message The message to include in the exception 56 | */ 57 | export function raiseServerException( 58 | status: number, 59 | message: string, 60 | ): never { 61 | throw new ServerException(message, status); 62 | } 63 | 64 | export async function tryCatchServerException< 65 | FN extends () => Promise | any, 66 | >( 67 | fn: FN, 68 | ): Promise<[ServerException | null, ReturnType | null]> { 69 | let err = null; 70 | let response = null; 71 | try { 72 | response = await fn(); 73 | } catch (error) { 74 | if (isServerException(error)) { 75 | err = error; 76 | } else { 77 | throw error; 78 | } 79 | } 80 | return [err, response]; 81 | } 82 | -------------------------------------------------------------------------------- /src/orm/settings/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | BaseConfig, 3 | BaseTypeConfig, 4 | BaseTypeInfo, 5 | } from "~/orm/shared/shared-types.ts"; 6 | import type { 7 | GenericSettings, 8 | SettingsBase, 9 | } from "~/orm/settings/settings-base.ts"; 10 | import type { InSpatialORM } from "~/orm/inspatial-orm.ts"; 11 | import type { InField, InFieldType } from "~/orm/field/field-def-types.ts"; 12 | import type { HookName } from "../orm-types.ts"; 13 | import type { InCloud } from "@inspatial/cloud/types"; 14 | 15 | export interface SettingsTypeInfo extends BaseTypeInfo { 16 | config: SettingsTypeConfig; 17 | } 18 | 19 | export interface SettingsTypeConfig extends BaseTypeConfig { 20 | } 21 | export type SettingsConfig = 22 | & BaseConfig 23 | & { 24 | actions?: Array>; 25 | hooks?: Partial>>>; 26 | }; 27 | export interface SettingsRow { 28 | id: string; 29 | settingsType: string; 30 | field: string; 31 | value: { 32 | value: any; 33 | type: InFieldType; 34 | }; 35 | updatedAt: number; 36 | } 37 | 38 | export type GlobalSettingsHook = () => void; 39 | export type SettingsHookFunction = { 40 | ( 41 | hookParams: 42 | & { 43 | inCloud: InCloud; 44 | orm: InSpatialORM; 45 | } 46 | & { 47 | [K in S["_name"] | "settings"]: S; 48 | }, 49 | ): Promise | void; 50 | }; 51 | export type SettingsHookDefinition = { 52 | name: string; 53 | description?: string; 54 | handler: SettingsHookFunction; 55 | }; 56 | 57 | export type SettingsActionDefinition = { 58 | key: string; 59 | description?: string; 60 | private?: boolean; 61 | action( 62 | actionParams: 63 | & { 64 | orm: InSpatialORM; 65 | } 66 | & { [K in S["_name"] | "settings"]: S } 67 | & { 68 | data: Record; 69 | }, 70 | ): Promise | any | void; 71 | params: Array; 72 | }; 73 | -------------------------------------------------------------------------------- /src/auth/actions/google/login-google.ts: -------------------------------------------------------------------------------- 1 | import { CloudAPIAction } from "~/api/cloud-action.ts"; 2 | import { raiseServerException } from "~/serve/server-exception.ts"; 3 | import { generateId } from "~/utils/mod.ts"; 4 | import type { AuthSettings } from "~/auth/settings/_auth-settings.type.ts"; 5 | 6 | export const signInWithGoogle = new CloudAPIAction("signInWithGoogle", { 7 | authRequired: false, 8 | description: "Redirect to Google OAuth2 login page", 9 | async run({ orm, inRequest, params }) { 10 | const { csrfToken, redirectTo } = params; 11 | const state = JSON.stringify({ 12 | redirectTo, 13 | csrfToken, 14 | type: "login", 15 | }); 16 | const authSettings = await orm.getSettings( 17 | "authSettings", 18 | ); 19 | const clientId = authSettings.googleClientId; 20 | if (!clientId) { 21 | raiseServerException( 22 | 400, 23 | "Google auth: Client ID not set in settings", 24 | ); 25 | } 26 | 27 | const redirectUri = `${ 28 | authSettings.hostname || inRequest.fullHost 29 | }/api?group=auth&action=googleAuthCallback`; 30 | 31 | const url = new URL( 32 | "https://accounts.google.com/o/oauth2/v2/auth", 33 | ); 34 | 35 | url.searchParams.set("client_id", clientId); 36 | url.searchParams.set("redirect_uri", redirectUri); 37 | url.searchParams.set("response_type", "code"); 38 | url.searchParams.set("scope", "openid email profile"); 39 | url.searchParams.set("prompt", "consent"); 40 | const nonce = generateId(30); 41 | url.searchParams.set("nonce", nonce); 42 | url.searchParams.set("state", state); 43 | 44 | return { 45 | redirect: url.toString(), 46 | }; 47 | }, 48 | params: [{ 49 | key: "redirectTo", 50 | label: "Redirect To", 51 | type: "TextField", 52 | required: false, 53 | description: "The redirect URI to use for the Google OAuth2 login", 54 | }, { 55 | key: "csrfToken", 56 | label: "CSRF Token", 57 | type: "DataField", 58 | required: false, 59 | description: "The CSRF token to use for the Google OAuth2 login", 60 | }], 61 | }); 62 | -------------------------------------------------------------------------------- /src/orm/migrate/migrate-utils.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | PgColumnDefinition, 3 | PgDataTypeDefinition, 4 | PostgresColumn, 5 | } from "~/orm/db/db-types.ts"; 6 | export function compareDataTypes( 7 | existing: PostgresColumn, 8 | newColumn: PgColumnDefinition, 9 | ): { 10 | from: PgDataTypeDefinition; 11 | to: PgDataTypeDefinition; 12 | } | undefined { 13 | const properties: Array = [ 14 | "dataType", 15 | "characterMaximumLength", 16 | "characterOctetLength", 17 | "numericPrecision", 18 | "numericPrecisionRadix", 19 | "numericScale", 20 | "datetimePrecision", 21 | "intervalType", 22 | "intervalPrecision", 23 | ]; 24 | const from: Record = {}; 25 | const to: Record = {}; 26 | let hasChanges = false; 27 | for (const property of properties) { 28 | if (property in newColumn && existing[property] !== newColumn[property]) { 29 | hasChanges = true; 30 | from[property] = existing[property]; 31 | to[property] = newColumn[property]; 32 | } 33 | } 34 | 35 | if (!hasChanges) { 36 | return; 37 | } 38 | if ("characterMaximumLength" in to && !("dataType" in to)) { 39 | (to as PgDataTypeDefinition)["dataType"] = "character varying"; 40 | } 41 | if ("numericPrecision" in to && !("dataType" in to)) { 42 | (to as PgDataTypeDefinition)["dataType"] = "numeric"; 43 | } 44 | if ("numericScale" in to && !("dataType" in to)) { 45 | (to as PgDataTypeDefinition)["dataType"] = "numeric"; 46 | } 47 | return { 48 | from: from as PgDataTypeDefinition, 49 | to: to as PgDataTypeDefinition, 50 | }; 51 | } 52 | export function compareNullable( 53 | existing: PostgresColumn, 54 | newColumn: PgColumnDefinition, 55 | ): { 56 | from: PgColumnDefinition["isNullable"]; 57 | to: PgColumnDefinition["isNullable"]; 58 | defaultValue?: PgColumnDefinition["columnDefault"]; 59 | } | undefined { 60 | if (existing.isNullable === newColumn.isNullable) { 61 | return; 62 | } 63 | 64 | return { 65 | from: existing.isNullable, 66 | to: newColumn.isNullable, 67 | defaultValue: newColumn.columnDefault, 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /src/terminal/box.ts: -------------------------------------------------------------------------------- 1 | import type { LineStyle } from "~/terminal/types.ts"; 2 | 3 | interface BoxLine { 4 | horizontal: string; 5 | vertical: string; 6 | topLeft: string; 7 | topRight: string; 8 | bottomLeft: string; 9 | bottomRight: string; 10 | verticalLeft: string; 11 | verticalRight: string; 12 | horizontalDown: string; 13 | horizontalUp: string; 14 | } 15 | 16 | type Box = Record; 17 | 18 | export const box: Box = { 19 | block: { 20 | horizontal: "█", 21 | vertical: "█", 22 | topLeft: "█", 23 | topRight: "█", 24 | bottomLeft: "█", 25 | bottomRight: "█", 26 | verticalLeft: "█", 27 | verticalRight: "█", 28 | horizontalDown: "█", 29 | horizontalUp: "█", 30 | }, 31 | double: { 32 | horizontal: "═", 33 | vertical: "║", 34 | topLeft: "╔", 35 | topRight: "╗", 36 | bottomLeft: "╚", 37 | bottomRight: "╝", 38 | verticalLeft: "╠", 39 | verticalRight: "╣", 40 | horizontalDown: "╦", 41 | horizontalUp: "╩", 42 | }, 43 | doubleSingle: { 44 | horizontal: "─", 45 | vertical: "│", 46 | topLeft: "╓", 47 | topRight: "╖", 48 | bottomLeft: "╙", 49 | bottomRight: "╜", 50 | verticalLeft: "╟", 51 | verticalRight: "╢", 52 | horizontalDown: "╥", 53 | horizontalUp: "╨", 54 | }, 55 | dotted: { 56 | horizontal: "⋯", 57 | vertical: "⋮", 58 | topLeft: "⋮", 59 | topRight: "⋮", 60 | bottomLeft: "⋮", 61 | bottomRight: "⋮", 62 | verticalLeft: "⋮", 63 | verticalRight: "⋮", 64 | horizontalDown: "⋮", 65 | horizontalUp: "⋮", 66 | }, 67 | standard: { 68 | horizontal: "─", 69 | vertical: "│", 70 | topLeft: "┌", 71 | topRight: "┐", 72 | bottomLeft: "└", 73 | bottomRight: "┘", 74 | verticalLeft: "┤", 75 | verticalRight: "├", 76 | horizontalDown: "┬", 77 | horizontalUp: "┴", 78 | }, 79 | thick: { 80 | topLeft: "┏", 81 | topRight: "┓", 82 | bottomLeft: "┗", 83 | bottomRight: "┛", 84 | vertical: "┃", 85 | horizontal: "━", 86 | verticalLeft: "┫", 87 | verticalRight: "┣", 88 | horizontalDown: "┳", 89 | horizontalUp: "┻", 90 | }, 91 | }; 92 | -------------------------------------------------------------------------------- /src/auth/actions/google/handle-google-login.ts: -------------------------------------------------------------------------------- 1 | import type { InCloud } from "~/in-cloud.ts"; 2 | import type { InSpatialORM } from "~/orm/inspatial-orm.ts"; 3 | import type { InRequest } from "~/serve/in-request.ts"; 4 | import type { InResponse } from "~/serve/in-response.ts"; 5 | import type { 6 | GoogleAccessTokenResponse, 7 | GoogleIdToken, 8 | } from "~/auth/providers/google/accessToken.ts"; 9 | import { raiseServerException } from "~/serve/server-exception.ts"; 10 | import type { User } from "../../entries/user/_user.type.ts"; 11 | 12 | export async function handleGoogleLogin(args: { 13 | accessToken: GoogleAccessTokenResponse; 14 | idToken: GoogleIdToken; 15 | csrfToken: string; 16 | redirectTo: string; 17 | inRequest: InRequest; 18 | inResponse: InResponse; 19 | orm: InSpatialORM; 20 | inCloud: InCloud; 21 | }) { 22 | const { email, emailVerified } = args.idToken; 23 | const { 24 | accessToken, 25 | idToken, 26 | redirectTo, 27 | orm, 28 | inCloud, 29 | inRequest, 30 | inResponse, 31 | } = args; 32 | const authHandler = inCloud.auth; 33 | if (!email || !emailVerified) { 34 | raiseServerException(401, "Google auth: Email not verified"); 35 | } 36 | const user = await orm.findEntry("user", [{ 37 | field: "email", 38 | op: "=", 39 | value: email, 40 | }]); 41 | if (!user) { 42 | raiseServerException(401, "Google auth: User not found"); 43 | } 44 | user.googleCredential = accessToken; 45 | user.googleAccessToken = accessToken.accessToken; 46 | user.googleRefreshToken = accessToken.refreshToken; 47 | user.googlePicture = idToken.picture; 48 | user.googleId = idToken.sub; 49 | user.googleAuthStatus = "authenticated"; 50 | await user.save(); 51 | await authHandler.createUserSession( 52 | user, 53 | inRequest, 54 | inResponse, 55 | ); 56 | const sessionId = inRequest.context.get("userSession"); 57 | if (!sessionId) { 58 | raiseServerException(401, "Google auth: Session not found"); 59 | } 60 | const redirectUrl = new URL(redirectTo); 61 | // redirectUrl.searchParams.set("sessionId", sessionId); 62 | return inResponse.redirect(redirectUrl.toString()); 63 | } 64 | -------------------------------------------------------------------------------- /src/files/mime-types/mime-types.ts: -------------------------------------------------------------------------------- 1 | import extensions from "./extensions.json" with { type: "json" }; 2 | import categories from "./categories.json" with { type: "json" }; 3 | import mimetypes from "./mimetypes.json" with { type: "json" }; 4 | import type { MimeTypeCategory } from "~/files/mime-types/file-types.ts"; 5 | 6 | interface ExtensionInfo { 7 | extension: string; 8 | description: string; 9 | mimeType: string; 10 | category: MimeTypeCategory; 11 | } 12 | 13 | class MimeTypes { 14 | static readonly mimetypes: Array = 15 | mimetypes as unknown as Array< 16 | ExtensionInfo 17 | >; 18 | static readonly extensions: Map = new Map( 19 | Object.entries(extensions as unknown as Record), 20 | ); 21 | static readonly categories: Map> = new Map( 22 | Object.entries( 23 | categories as unknown as Record>, 24 | ), 25 | ); 26 | static get categoryNames(): Array { 27 | return Array.from(this.categories.keys()) as Array; 28 | } 29 | static getMimeTypeByFileName( 30 | fileName: string, 31 | ): string | undefined { 32 | const extension = fileName.split(".").pop(); 33 | if (!extension) return undefined; 34 | return this.extensions.get(extension)?.mimeType; 35 | } 36 | static getMimeTypeByExtension(extension: string): string | undefined { 37 | return this.extensions.get(extension)?.mimeType; 38 | } 39 | 40 | static getCategory(extension: string): MimeTypeCategory | undefined { 41 | return this.extensions.get(extension)?.category; 42 | } 43 | static getDescription(extension: string): string | undefined { 44 | return this.extensions.get(extension)?.description; 45 | } 46 | 47 | static getExtensionsByCategory( 48 | category: MimeTypeCategory, 49 | ): Array | undefined { 50 | return this.categories.get(category); 51 | } 52 | 53 | static getExtensionsByMimeType( 54 | mimeType: string, 55 | ): ExtensionInfo | undefined { 56 | return this.mimetypes.find((item) => item.mimeType === mimeType); 57 | } 58 | } 59 | 60 | export default MimeTypes; 61 | -------------------------------------------------------------------------------- /src/files/mime-types/generate.ts: -------------------------------------------------------------------------------- 1 | import { convertString } from "~/utils/mod.ts"; 2 | import mimetypes from "./mimetypes.json" with { type: "json" }; 3 | 4 | async function generateTypes() { 5 | const allData = getAllData(); 6 | 7 | let fileTypes = ""; 8 | let output = ""; 9 | 10 | fileTypes += `// This file is auto-generated. Do not edit.\n`; 11 | fileTypes += `// Generated on ${new Date().toISOString()}\n\n`; 12 | 13 | fileTypes += "export type MimeTypeCategory = keyof FileTypes;\n"; 14 | fileTypes += "export type FileTypes = {\n"; 15 | let allTypes = "export type FileType = \n"; 16 | for (const [key, value] of Object.entries(allData)) { 17 | const typeName = `${convertString(key, "title")}FileType`; 18 | allTypes += ` | ${typeName}\n`; 19 | fileTypes += ` ${key}: Array<${typeName}>;\n`; 20 | output += `export type ${convertString(key, "title")}FileType = \n`; 21 | 22 | for (const item of value) { 23 | output += ` | "${item}"\n`; 24 | } 25 | } 26 | output += `\n\n`; 27 | fileTypes += `};\n\n`; 28 | await Deno.writeTextFile("file-types.ts", fileTypes + output + allTypes); 29 | } 30 | 31 | async function generateExtensions() { 32 | const allData: Record = {}; 33 | for (const item of mimetypes) { 34 | allData[item.extension] = item; 35 | } 36 | await Deno.writeTextFile("extensions.json", JSON.stringify(allData, null, 2)); 37 | } 38 | 39 | async function generateCategories() { 40 | const allData: Record = {}; 41 | for (const item of mimetypes) { 42 | if (!allData[item.category]) { 43 | allData[item.category] = []; 44 | } 45 | allData[item.category].push(item); 46 | } 47 | await Deno.writeTextFile("categories.json", JSON.stringify(allData, null, 2)); 48 | } 49 | 50 | function getAllData() { 51 | const allData: Record = {}; 52 | for (const item of mimetypes) { 53 | if (!allData[item.category]) { 54 | allData[item.category] = []; 55 | } 56 | allData[item.category].push(item.extension); 57 | } 58 | return allData; 59 | } 60 | 61 | if (import.meta.main) { 62 | await generateTypes(); 63 | await generateExtensions(); 64 | await generateCategories(); 65 | // 66 | } 67 | -------------------------------------------------------------------------------- /src/in-live/types.ts: -------------------------------------------------------------------------------- 1 | import type { SessionData } from "../auth/types.ts"; 2 | 3 | /** 4 | * A user connected to the realtime server. 5 | */ 6 | export interface InLiveUser { 7 | /** 8 | * The id of the user. 9 | */ 10 | id: string; 11 | /** 12 | * The name of the user. 13 | */ 14 | name?: string; 15 | } 16 | 17 | /** 18 | * A client connected to the realtime server. 19 | */ 20 | export interface InLiveClient { 21 | /** 22 | * The id of the client. 23 | */ 24 | id: string; 25 | 26 | /** 27 | * The websocket connection to the client. 28 | */ 29 | socket: WebSocket; 30 | /** 31 | * The user associated with the client. 32 | */ 33 | user: SessionData; 34 | 35 | /** 36 | * The rooms the client is in 37 | */ 38 | rooms: Set; 39 | } 40 | 41 | /** 42 | * A definition of a room. 43 | */ 44 | export interface InLiveRoomDef { 45 | /** 46 | * The name of the room. 47 | */ 48 | roomName: string; 49 | /** 50 | * A description of the room 51 | */ 52 | description?: string; 53 | } 54 | 55 | /** 56 | * A message received from a client. 57 | */ 58 | export type InLiveClientMessage< 59 | T extends Record = Record, 60 | > = { 61 | /** 62 | * The type of message. 63 | */ 64 | type: "join" | "leave" | "message"; 65 | /** 66 | * The name of the room the message is for. 67 | */ 68 | roomName: string; 69 | /** 70 | * The data of the message. 71 | */ 72 | data: T; 73 | }; 74 | 75 | /** 76 | * A message sent to a broadcast channel to synchronize server instances. 77 | */ 78 | export type InLiveBroadcastMessage< 79 | T extends Record = Record, 80 | > = { 81 | accountId: string; 82 | /** 83 | * The name of the room to broadcast to. 84 | */ 85 | roomName: string; 86 | /** 87 | * The event to broadcast. 88 | */ 89 | event: string; 90 | /** 91 | * The data to broadcast. 92 | */ 93 | data: T; 94 | }; 95 | 96 | /** 97 | * A union of all possible realtime messages. 98 | */ 99 | export type InLiveMessage = InLiveClientMessage | InLiveBroadcastMessage; 100 | -------------------------------------------------------------------------------- /src/serve/path-handler.ts: -------------------------------------------------------------------------------- 1 | import type { InRequest } from "~/serve/in-request.ts"; 2 | import type { InResponse } from "~/serve/in-response.ts"; 3 | import type { InCloud } from "@inspatial/cloud/types"; 4 | export class RequestPathHandler { 5 | name: string; 6 | description: string; 7 | match: RegExp; 8 | handler: ( 9 | app: InCloud, 10 | inRequest: InRequest, 11 | inResponse: InResponse, 12 | ) => 13 | | Promise 14 | | HandlerResponse; 15 | 16 | constructor( 17 | name: string, 18 | description: string, 19 | match: RegExp, 20 | handler: ( 21 | app: InCloud, 22 | inRequest: InRequest, 23 | inResponse: InResponse, 24 | ) => 25 | | Promise 26 | | HandlerResponse, 27 | ) { 28 | this.name = name; 29 | this.description = description; 30 | this.match = match; 31 | this.handler = handler; 32 | } 33 | } 34 | 35 | /** 36 | * A union of all possible response types from a PathHandler. 37 | */ 38 | export type HandlerResponse = 39 | | void 40 | | string 41 | | Record 42 | | InResponse 43 | | Response 44 | | Record[] 45 | | number; 46 | /** 47 | * A handler for a path. 48 | * This is used to define a handler for a specific path. 49 | */ 50 | export type PathHandler = { 51 | /** 52 | * The name of the path handler. 53 | */ 54 | name: string; 55 | /** 56 | * A description of what the path handler does. 57 | */ 58 | description: string; 59 | 60 | /** 61 | * The path that the handler should be called for. 62 | * This can be a string or an array of strings and must be unique. 63 | */ 64 | match: RegExp; 65 | 66 | /** 67 | * The handler for the path. 68 | * This is called when a request is made to the path. 69 | * It receives the path, the request object, and the response object. 70 | * It can modify the response object as needed. 71 | * If the handler returns a response, the response will be sent to the client as-is. 72 | */ 73 | handler: ( 74 | inCloud: InCloud, 75 | inRequest: InRequest, 76 | inResponse: InResponse, 77 | ) => 78 | | Promise 79 | | HandlerResponse; 80 | }; 81 | -------------------------------------------------------------------------------- /src/orm/db/postgres/in-pg/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { normalizeVirtualPath } from "./convert.ts"; 2 | import type { PGMem } from "./pgMem.ts"; 3 | 4 | export class ExitStatus { 5 | name = "ExitStatus"; 6 | message: string; 7 | status: number; 8 | constructor(status: number) { 9 | this.message = `Program terminated with exit(${status})`; 10 | this.status = status; 11 | } 12 | } 13 | 14 | export class ExceptionInfo { 15 | excPtr: number; 16 | ptr: number; 17 | mem: PGMem; 18 | constructor(excPtr: number, mem: PGMem) { 19 | this.mem = mem; 20 | this.excPtr = excPtr; 21 | this.ptr = excPtr - 24; 22 | } 23 | set_type(type: number) { 24 | this.mem.HEAPU32[(this.ptr + 4) >> 2] = type; 25 | } 26 | get_type() { 27 | return this.mem.HEAPU32[(this.ptr + 4) >> 2]; 28 | } 29 | set_destructor(destructor: number) { 30 | this.mem.HEAPU32[(this.ptr + 8) >> 2] = destructor; 31 | } 32 | get_destructor() { 33 | return this.mem.HEAPU32[(this.ptr + 8) >> 2]; 34 | } 35 | set_caught(caught: boolean | number) { 36 | caught = caught ? 1 : 0; 37 | this.mem.HEAP8[this.ptr + 12] = caught; 38 | } 39 | get_caught() { 40 | return this.mem.HEAP8[this.ptr + 12] != 0; 41 | } 42 | set_rethrown(rethrown: boolean | number) { 43 | rethrown = rethrown ? 1 : 0; 44 | this.mem.HEAP8[this.ptr + 13] = rethrown; 45 | } 46 | get_rethrown() { 47 | return this.mem.HEAP8[this.ptr + 13] != 0; 48 | } 49 | init(type: number, destructor: number) { 50 | this.set_adjusted_ptr(0); 51 | this.set_type(type); 52 | this.set_destructor(destructor); 53 | } 54 | set_adjusted_ptr(adjustedPtr: number) { 55 | this.mem.HEAPU32[(this.ptr + 16) >> 2] = adjustedPtr; 56 | } 57 | get_adjusted_ptr() { 58 | return this.mem.HEAPU32[(this.ptr + 16) >> 2]; 59 | } 60 | } 61 | 62 | export function getTempDirBase() { 63 | let path = Deno.makeTempFileSync(); 64 | if (Deno.build.os === "windows") { 65 | const driveLetter = path.match(/^[a-zA-Z]:/)?.[0] || ""; 66 | path = `${driveLetter}${normalizeVirtualPath(path)}`; 67 | } 68 | Deno.removeSync(path); 69 | const parts = path.split("/"); 70 | parts.pop(); 71 | const tmpdir = parts.join("/"); 72 | return tmpdir; 73 | } 74 | -------------------------------------------------------------------------------- /src/orm/api-actions/orm-group.ts: -------------------------------------------------------------------------------- 1 | import { CloudAPIAction } from "~/api/cloud-action.ts"; 2 | import type { SettingsTypeInfo } from "~/orm/settings/types.ts"; 3 | import type { EntryTypeInfo } from "~/orm/entry/types.ts"; 4 | import type { SessionData } from "~/auth/types.ts"; 5 | 6 | export const entryTypesInfo = new CloudAPIAction("entryTypes", { 7 | description: "Get EntryType Definitions", 8 | label: "Entry Types", 9 | run({ inCloud, inRequest }): Array { 10 | const user = inRequest.context.get("user"); 11 | if (!user) { 12 | return []; 13 | } 14 | const role = inCloud.roles.getRole(user.role); 15 | return Array.from( 16 | role.entryTypes.values().map((entryType) => entryType.info), 17 | ) as Array; 18 | }, 19 | params: [], 20 | }); 21 | 22 | export const settingsTypesInfo = new CloudAPIAction("settingsTypes", { 23 | description: "Get SettingsType Definitions", 24 | label: "Settings Types", 25 | run({ inCloud, inRequest }): Array { 26 | const user = inRequest.context.get("user"); 27 | if (!user) { 28 | return []; 29 | } 30 | const role = inCloud.roles.getRole(user.role); 31 | return Array.from( 32 | role.settingsTypes.values().map((settingsType) => settingsType.info), 33 | ) as Array; 34 | }, 35 | params: [], 36 | }); 37 | 38 | export const generateInterfaces = new CloudAPIAction("generateInterfaces", { 39 | description: "Generate Entry Typescript Interfaces", 40 | label: "Generate Interfaces", 41 | async run({ orm }): Promise<{ 42 | generatedEntries: Array; 43 | generatedSettings: Array; 44 | }> { 45 | return await orm.generateInterfaces(); 46 | }, 47 | params: [], 48 | }); 49 | 50 | export const getClientInterfaces = new CloudAPIAction("getClientInterfaces", { 51 | description: "Get Client Typescript Interfaces", 52 | label: "Get Client Interfaces", 53 | run({ orm, inResponse }) { 54 | const result = orm.generateClientInterfaces(); 55 | return inResponse.setFile({ 56 | content: result, 57 | fileName: "client-interfaces.ts", 58 | download: true, 59 | }); 60 | }, 61 | params: [], 62 | }); 63 | -------------------------------------------------------------------------------- /src/files/mime-types/file-types.ts: -------------------------------------------------------------------------------- 1 | // This file is auto-generated. Do not edit. 2 | // Generated on 2025-04-27T16:05:14.575Z 3 | 4 | export type MimeTypeCategory = keyof FileTypes; 5 | export type FileTypes = { 6 | audio: Array; 7 | image: Array; 8 | video: Array; 9 | document: Array; 10 | application: Array; 11 | code: Array; 12 | text: Array; 13 | font: Array; 14 | archive: Array; 15 | }; 16 | 17 | export type AudioFileType = 18 | | "aac" 19 | | "mid" 20 | | "midi" 21 | | "mp3" 22 | | "oga" 23 | | "opus" 24 | | "wav" 25 | | "weba"; 26 | export type ImageFileType = 27 | | "apng" 28 | | "avif" 29 | | "bmp" 30 | | "gif" 31 | | "ico" 32 | | "jpeg" 33 | | "jpg" 34 | | "png" 35 | | "svg" 36 | | "tif" 37 | | "tiff" 38 | | "webp"; 39 | export type VideoFileType = 40 | | "avi" 41 | | "mp4" 42 | | "mpeg" 43 | | "ogv" 44 | | "ts" 45 | | "webm" 46 | | "3gp" 47 | | "3g2"; 48 | export type DocumentFileType = 49 | | "azw" 50 | | "csv" 51 | | "doc" 52 | | "docx" 53 | | "epub" 54 | | "odp" 55 | | "ods" 56 | | "odt" 57 | | "pdf" 58 | | "ppt" 59 | | "pptx" 60 | | "rtf" 61 | | "xls" 62 | | "xlsx"; 63 | export type ApplicationFileType = 64 | | "bin" 65 | | "jsonld" 66 | | "mpkg" 67 | | "ogx" 68 | | "xhtml"; 69 | export type CodeFileType = 70 | | "csh" 71 | | "js" 72 | | "json" 73 | | "mjs" 74 | | "php" 75 | | "sh" 76 | | "xml"; 77 | export type TextFileType = 78 | | "css" 79 | | "htm" 80 | | "html" 81 | | "ics" 82 | | "txt"; 83 | export type FontFileType = 84 | | "eot" 85 | | "otf" 86 | | "ttf" 87 | | "woff" 88 | | "woff2"; 89 | export type ArchiveFileType = 90 | | "gz" 91 | | "jar" 92 | | "rar" 93 | | "tar" 94 | | "zip" 95 | | "7z" 96 | | "arc"; 97 | 98 | export type FileType = 99 | | AudioFileType 100 | | ImageFileType 101 | | VideoFileType 102 | | DocumentFileType 103 | | ApplicationFileType 104 | | CodeFileType 105 | | TextFileType 106 | | FontFileType 107 | | ArchiveFileType; 108 | -------------------------------------------------------------------------------- /src/orm/db/postgres/README.md: -------------------------------------------------------------------------------- 1 | ## Memory Tuning 2 | 3 | Postgres is a memory hog. It will use as much memory as you give it. The more 4 | memory you give it, the faster it will run. The less memory you give it, the 5 | slower it will run. The default configuration is set to use a very small amount 6 | of memory. This is because Postgres is designed to run on a wide variety of 7 | hardware configurations. It is up to you to tune Postgres to use the amount of 8 | memory that is appropriate for your hardware. 9 | 10 | ### Shared Buffers 11 | 12 | `shared_buffers` is the amount of memory that Postgres will use for caching 13 | data. The default value is 128MB. This is a very small amount of memory. You 14 | should set this to at least 25% of your total system memory. If you have 4GB of 15 | memory, you should set this to 1GB. If you have 8GB of memory, you should set 16 | this to 2GB etc. 17 | 18 | The default value for this parameter, which is set in postgresql.conf, is: 19 | 20 | ```conf 21 | #shared_buffers = 128MB 22 | ``` 23 | 24 | ### Work Memory 25 | 26 | `work_mem` is the amount of memory that Postgres will use for sorting and 27 | hashing operations. The default value is 4MB. 28 | 29 | ``` 30 | Total RAM * 0.25 / max_connections 31 | ``` 32 | 33 | so if you have 4GB of memory and 100 connections, you should set this to 10MB. 34 | 35 | ### Maintenance Work Memory 36 | 37 | The maintenance_work_mem parameter basically provides the maximum amount of 38 | memory to be used by maintenance operations like vacuum, create index, and alter 39 | table add foreign key operations. 40 | 41 | The default value for this parameter, which is set in postgresql.conf, is: 42 | 43 | ```conf 44 | #maintenance_work_mem = 64MB 45 | ``` 46 | 47 | It’s recommended to set this value higher than work_mem; this can improve 48 | performance for vacuuming. In general it should be: 49 | 50 | ``` 51 | Total RAM * 0.05 52 | ``` 53 | 54 | ### Effective Cache Size 55 | 56 | `effective_cache_size` is the amount of memory that Postgres will use for 57 | caching data that is stored on disk. The default value is 128MB. You should set 58 | this to at least 50% of your total system memory. If you have 4GB of memory, you 59 | should set this to 2GB. If you have 8GB of memory, you should set this to 4GB 60 | etc. 61 | -------------------------------------------------------------------------------- /src/auth/entries/user/user-entry.ts: -------------------------------------------------------------------------------- 1 | import { EntryType } from "~/orm/entry/entry-type.ts"; 2 | import type { User } from "./_user.type.ts"; 3 | import { findAccounts } from "./actions/find-accounts.ts"; 4 | import { userFields } from "./fields/fields.ts"; 5 | import { googleFields } from "./fields/google-fields.ts"; 6 | import { setPassword } from "./actions/set-password.ts"; 7 | import { validatePassword } from "./actions/validate-password.ts"; 8 | import { generateApiToken } from "./actions/generate-api-token.ts"; 9 | import { generateResetToken } from "./actions/generate-reset-token.ts"; 10 | 11 | export const userEntry = new EntryType("user", { 12 | titleField: "fullName", 13 | systemGlobal: true, 14 | defaultListFields: ["firstName", "lastName", "email", "systemAdmin"], 15 | description: "A user of the system", 16 | searchFields: ["email"], 17 | fields: [ 18 | ...userFields, 19 | ...googleFields, 20 | ], 21 | fieldGroups: [{ 22 | key: "personal", 23 | label: "Personal Information", 24 | description: "Basic information about the user", 25 | fields: ["profilePicture", "firstName", "lastName", "fullName", "email"], 26 | }, { 27 | key: "security", 28 | label: "Security", 29 | description: "Security related information", 30 | fields: ["systemAdmin", "apiToken"], 31 | }, { 32 | key: "google", 33 | label: "Google Account", 34 | description: "Google account information", 35 | fields: ["googlePicture", "googleAuthStatus"], 36 | }], 37 | actions: [ 38 | setPassword, 39 | validatePassword, 40 | generateApiToken, 41 | generateResetToken, 42 | findAccounts, 43 | ], 44 | 45 | hooks: { 46 | beforeUpdate: [{ 47 | name: "setFullName", 48 | description: "Set the full name of the user", 49 | handler({ 50 | user, 51 | }) { 52 | user.fullName = `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim(); 53 | }, 54 | }], 55 | beforeDelete: [{ 56 | name: "deleteUserSessions", 57 | description: "Delete all user sessions", 58 | async handler({ orm, user }) { 59 | await orm.systemDb.deleteRows("entryUserSession", [{ 60 | field: "user", 61 | op: "=", 62 | value: user.id, 63 | }]); 64 | }, 65 | }], 66 | }, 67 | }); 68 | -------------------------------------------------------------------------------- /src/in-queue/entry-types/in-task/fields.ts: -------------------------------------------------------------------------------- 1 | import type { InField } from "~/orm/field/field-def-types.ts"; 2 | 3 | export const inTaskFields: Array = [{ 4 | key: "taskType", 5 | label: "Task Type", 6 | type: "ChoicesField", 7 | required: true, 8 | choices: [{ 9 | key: "entry", 10 | label: "Entry", 11 | }, { 12 | key: "settings", 13 | label: "Settings", 14 | }], 15 | }, { 16 | key: "typeKey", 17 | label: "Entry/Settings Name", 18 | type: "DataField", 19 | dependsOn: { 20 | field: "taskType", 21 | value: ["entry", "settings"], 22 | }, 23 | }, { 24 | key: "entryId", 25 | label: "Entry ID", 26 | description: "The ID of the entry to run the action on", 27 | type: "DataField", 28 | dependsOn: { 29 | field: "taskType", 30 | value: "entry", 31 | }, 32 | }, { 33 | key: "group", 34 | label: "Group", 35 | type: "DataField", 36 | dependsOn: { 37 | field: "taskType", 38 | value: "app", 39 | }, 40 | }, { 41 | key: "actionName", 42 | label: "Action Name", 43 | type: "DataField", 44 | required: true, 45 | }, { 46 | key: "status", 47 | label: "Status", 48 | type: "ChoicesField", 49 | defaultValue: "queued", 50 | required: true, 51 | readOnly: false, 52 | choices: [{ 53 | key: "queued", 54 | label: "Queued", 55 | color: "muted", 56 | }, { 57 | key: "running", 58 | label: "Running", 59 | color: "warning", 60 | }, { 61 | key: "cancelled", 62 | label: "Cancelled", 63 | color: "error", 64 | }, { 65 | key: "completed", 66 | label: "Completed", 67 | color: "success", 68 | }, { 69 | key: "failed", 70 | label: "Failed", 71 | color: "error", 72 | }], 73 | }, { 74 | key: "startTime", 75 | label: "Start Time", 76 | type: "TimeStampField", 77 | readOnly: true, 78 | showTime: true, 79 | }, { 80 | key: "endTime", 81 | label: "End Time", 82 | type: "TimeStampField", 83 | readOnly: true, 84 | showTime: true, 85 | }, { 86 | key: "taskData", 87 | label: "Task Data", 88 | type: "JSONField", 89 | readOnly: true, 90 | }, { 91 | key: "resultData", 92 | label: "Result Data", 93 | type: "JSONField", 94 | readOnly: true, 95 | }, { 96 | key: "errorInfo", 97 | type: "TextField", 98 | readOnly: true, 99 | }]; 100 | -------------------------------------------------------------------------------- /src/auth/actions/register-account.ts: -------------------------------------------------------------------------------- 1 | import { CloudAPIAction } from "~/api/cloud-action.ts"; 2 | import { raiseCloudException } from "~/serve/exeption/cloud-exception.ts"; 3 | import type { User } from "~/auth/entries/user/_user.type.ts"; 4 | import type { Account } from "~/auth/entries/account/_account.type.ts"; 5 | import { SystemSettings } from "../../extension/settings/_system-settings.type.ts"; 6 | 7 | export const registerAccount = new CloudAPIAction("registerAccount", { 8 | label: "Register Account", 9 | description: "Create a new acount and assign the given user as the owner.", 10 | authRequired: false, 11 | async run({ inCloud, orm, params, inRequest, inResponse }) { 12 | const { enableSignup } = await orm.getSettings( 13 | "systemSettings", 14 | ); 15 | if (!enableSignup) { 16 | raiseCloudException("User signup is disabled", { 17 | type: "warning", 18 | }); 19 | } 20 | const { firstName, lastName, email, password } = params; 21 | const existingUser = await orm.findEntry("user", [{ 22 | field: "email", 23 | op: "=", 24 | value: email, 25 | }]); 26 | if (existingUser) { 27 | raiseCloudException("User already exists", { 28 | type: "warning", 29 | }); 30 | } 31 | const user = orm.getNewEntry("user"); 32 | user.update({ 33 | firstName, 34 | lastName, 35 | email, 36 | }); 37 | await user.save(); 38 | await user.runAction("setPassword", { password }); 39 | const account = await orm.createEntry("account", { 40 | users: [{ user: user.id }], 41 | }); 42 | await account.enqueueAction("initialize"); 43 | return await inCloud.auth.createUserSession(user, inRequest, inResponse); 44 | }, 45 | params: [{ 46 | key: "firstName", 47 | label: "First Name", 48 | description: "First name of the user", 49 | type: "DataField", 50 | required: true, 51 | }, { 52 | key: "lastName", 53 | label: "Last Name", 54 | description: "Last name of the user", 55 | type: "DataField", 56 | required: true, 57 | }, { 58 | key: "email", 59 | label: "Email", 60 | description: "Email of the user", 61 | type: "EmailField", 62 | required: true, 63 | }, { 64 | key: "password", 65 | label: "Password", 66 | description: "Password of the user", 67 | type: "PasswordField", 68 | required: true, 69 | }], 70 | }); 71 | -------------------------------------------------------------------------------- /src/in-queue/entry-types/in-task/_in-task-global.type.ts: -------------------------------------------------------------------------------- 1 | import type { EntryBase } from "@inspatial/cloud/types"; 2 | 3 | export interface InTaskGlobal extends EntryBase { 4 | _name: "inTaskGlobal"; 5 | /** 6 | * **Task Type** (ChoicesField) 7 | * @type {'entry' | 'settings'} 8 | * @required true 9 | */ 10 | taskType: "entry" | "settings"; 11 | /** 12 | * **Entry/Settings Name** (DataField) 13 | * @type {string} 14 | */ 15 | typeKey?: string; 16 | /** 17 | * **Entry ID** (DataField) 18 | * @description The ID of the entry to run the action on 19 | * @type {string} 20 | */ 21 | entryId?: string; 22 | /** 23 | * **Group** (DataField) 24 | * @type {string} 25 | */ 26 | group?: string; 27 | /** 28 | * **Action Name** (DataField) 29 | * @type {string} 30 | * @required true 31 | */ 32 | actionName: string; 33 | /** 34 | * **Status** (ChoicesField) 35 | * @type {'queued' | 'running' | 'cancelled' | 'completed' | 'failed'} 36 | * @required true 37 | */ 38 | status: "queued" | "running" | "cancelled" | "completed" | "failed"; 39 | /** 40 | * **Start Time** (TimeStampField) 41 | * @type {number} 42 | */ 43 | startTime?: number; 44 | /** 45 | * **End Time** (TimeStampField) 46 | * @type {number} 47 | */ 48 | endTime?: number; 49 | /** 50 | * **Task Data** (JSONField) 51 | * @type {Record} 52 | */ 53 | taskData?: Record; 54 | /** 55 | * **Result Data** (JSONField) 56 | * @type {Record} 57 | */ 58 | resultData?: Record; 59 | /** 60 | * **Error Info** (TextField) 61 | * @type {string} 62 | */ 63 | errorInfo?: string; 64 | /** 65 | * **InTask Global** (IDField) 66 | * @type {string} 67 | * @required true 68 | */ 69 | id: string; 70 | /** 71 | * **Created At** (TimeStampField) 72 | * @description The date and time this entry was created 73 | * @type {number} 74 | * @required true 75 | */ 76 | createdAt: number; 77 | /** 78 | * **Updated At** (TimeStampField) 79 | * @description The date and time this entry was last updated 80 | * @type {number} 81 | * @required true 82 | */ 83 | updatedAt: number; 84 | runAction( 85 | actionName: N, 86 | ): InTaskGlobalActionMap[N]["return"]; 87 | } 88 | type InTaskGlobalActionMap = { 89 | runTask: { 90 | return: Promise; 91 | }; 92 | }; 93 | -------------------------------------------------------------------------------- /src/serve/request-handler.ts: -------------------------------------------------------------------------------- 1 | import { InRequest } from "~/serve/in-request.ts"; 2 | import type { PathHandler } from "~/serve/path-handler.ts"; 3 | import { InResponse } from "~/serve/in-response.ts"; 4 | import type { ExtensionManager } from "~/extension/extension-manager.ts"; 5 | import { handleException } from "~/serve/exeption/handle-exception.ts"; 6 | import type { InCloud } from "~/in-cloud.ts"; 7 | import { raiseServerException } from "./server-exception.ts"; 8 | import { staticFilesHandler } from "../static/staticPathHandler.ts"; 9 | 10 | export async function requestHandler( 11 | request: Request, 12 | inCloud: InCloud, 13 | extensionManager: ExtensionManager, 14 | ): Promise { 15 | const inRequest = new InRequest( 16 | request, 17 | ); 18 | for (const { handler } of extensionManager.requestLifecycle.setup) { 19 | await handler(inRequest); 20 | } 21 | 22 | const inResponse = new InResponse(); 23 | try { 24 | for (const middleware of extensionManager.middlewares.values()) { 25 | const response = await middleware.handler( 26 | inCloud, 27 | inRequest, 28 | inResponse, 29 | ); 30 | 31 | if (response instanceof InResponse) { 32 | return response.respond(); 33 | } 34 | if (response instanceof Response) { 35 | return response; 36 | } 37 | } 38 | 39 | if (inRequest.method === "OPTIONS") { 40 | return inResponse.respond(); 41 | } 42 | 43 | const currentPath = inRequest.path; 44 | 45 | let pathHandler: PathHandler | undefined = undefined; 46 | for (const handler of extensionManager.pathHandlers) { 47 | if (handler.match.test(currentPath)) { 48 | pathHandler = handler; 49 | break; 50 | } 51 | } 52 | if (!pathHandler) { 53 | // default to static files handler if no path handler matches 54 | pathHandler = staticFilesHandler; 55 | } 56 | const response = await pathHandler.handler( 57 | inCloud, 58 | inRequest, 59 | inResponse, 60 | ); 61 | if (response instanceof InResponse) { 62 | return response.respond(); 63 | } 64 | if (response instanceof Response) { 65 | return response; 66 | } 67 | if (response) { 68 | inResponse.setContent(response); 69 | } 70 | return inResponse.respond(); 71 | } catch (e) { 72 | return await handleException( 73 | e, 74 | inResponse, 75 | extensionManager.exceptionHandlers, 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/files/entries/cloud-file.ts: -------------------------------------------------------------------------------- 1 | import { EntryType } from "@inspatial/cloud"; 2 | 3 | import { convertString } from "~/utils/mod.ts"; 4 | import type { CloudFile } from "./_cloud-file.type.ts"; 5 | import type { EntryConfig } from "~/orm/entry/types.ts"; 6 | import MimeTypes from "../mime-types/mime-types.ts"; 7 | const config = { 8 | label: "File", 9 | titleField: "fileName", 10 | defaultListFields: ["fileName", "fileType", "fileSize"], 11 | fields: [{ 12 | key: "fileName", 13 | label: "File Name", 14 | type: "DataField", 15 | required: true, 16 | }, { 17 | key: "fileSize", 18 | label: "File Size", 19 | format: "fileSize", 20 | type: "IntField", 21 | readOnly: true, 22 | required: true, 23 | }, { 24 | key: "fileType", 25 | label: "File Type", 26 | readOnly: true, 27 | type: "ChoicesField", 28 | choices: MimeTypes.categoryNames.map((category) => ({ 29 | key: category, 30 | label: convertString(category, "title"), 31 | })), 32 | }, { 33 | key: "fileExtension", 34 | label: "File Extension", 35 | readOnly: true, 36 | type: "ChoicesField", 37 | choices: MimeTypes.mimetypes.map((mimeType) => ({ 38 | key: mimeType.extension, 39 | label: mimeType.extension.toUpperCase(), 40 | })), 41 | }, { 42 | key: "mimeType", 43 | label: "Mime Type", 44 | readOnly: true, 45 | type: "DataField", 46 | }, { 47 | key: "fileTypeDescription", 48 | label: "File Type Description", 49 | readOnly: true, 50 | type: "DataField", 51 | }, { 52 | key: "filePath", 53 | label: "File Path", 54 | type: "TextField", 55 | hidden: true, 56 | readOnly: true, 57 | required: true, 58 | }], 59 | hooks: { 60 | afterDelete: [{ 61 | name: "deleteFile", 62 | async handler({ 63 | cloudFile, 64 | }) { 65 | const path = cloudFile.filePath; 66 | try { 67 | await Deno.remove(path); 68 | } catch (e) { 69 | if (e instanceof Deno.errors.NotFound) { 70 | console.warn("File not found for deletion:", path); 71 | return; 72 | } 73 | throw e; 74 | } 75 | }, 76 | }], 77 | }, 78 | } as EntryConfig; 79 | export const cloudFile = new EntryType("cloudFile", config); 80 | export const globalCloudFile = new EntryType("globalCloudFile", { 81 | ...config, 82 | label: "System File", 83 | description: "A shared system file", 84 | systemGlobal: true, 85 | }); 86 | -------------------------------------------------------------------------------- /src/types/serve-types.ts: -------------------------------------------------------------------------------- 1 | import type { LogType } from "~/in-log/types.ts"; 2 | 3 | /** 4 | * The HTTP request method. 5 | */ 6 | export type RequestMethod = 7 | | "GET" 8 | | "POST" 9 | | "PUT" 10 | | "DELETE" 11 | | "PATCH" 12 | | "OPTIONS" 13 | | "HEAD" 14 | | "CONNECT" 15 | | "TRACE"; 16 | 17 | /** 18 | * Exception handler function signature. 19 | * @param error The error that was thrown by the server 20 | * @returns An {@link ExceptionHandlerResponse} with a `clientMessage` and a `serverMessage` 21 | * 22 | * You can return a string or an object for both the clientMessage and serverMessage. 23 | * 24 | * The `clientMessage` is the message that will be sent to the client in the response body 25 | * and should not contain any sensitive information. 26 | * 27 | * The `serverMessage` is the message that will be logged by the server. 28 | * 29 | * @example 30 | * ```ts 31 | * const handler: ExceptionHandler = { 32 | * name: "My Exception Handler", 33 | * handler: (error) => { 34 | * if(error instanceof MyCustomError){ 35 | * 36 | * return { 37 | * clientMessage: "A custom error occurred", 38 | * serverMessage: { 39 | * error: "My app broke again", 40 | * details: error.details 41 | * } 42 | * } 43 | * } 44 | * } 45 | * ``` 46 | */ 47 | export type ExceptionHandler = { 48 | name: string; 49 | description?: string; 50 | handler: ( 51 | error: unknown, 52 | ) => 53 | | ExceptionHandlerResponse 54 | | void 55 | | Promise; 56 | }; 57 | 58 | /** 59 | * The response object returned by {@link ExceptionHandler} functions 60 | */ 61 | export interface ExceptionHandlerResponse { 62 | /** 63 | * The message to send to the client 64 | */ 65 | clientMessage?: Record | string; 66 | 67 | /** 68 | * The message to log on the server 69 | */ 70 | serverMessage?: { 71 | subject?: string; 72 | type?: LogType; 73 | content: Record | string; 74 | }; 75 | /** 76 | * The http status code to send to the client 77 | * @default 500 78 | * **Note:** This can only be set once and will be ignored (a server warning will be logged) if set more than once 79 | */ 80 | status?: number; 81 | 82 | /** 83 | * The http status text to send to the client 84 | * @default "Internal Server Error" 85 | * **Note:** This can only be set once and will be ignored (a server warning will be logged) if set more than once 86 | */ 87 | statusText?: string; 88 | } 89 | -------------------------------------------------------------------------------- /src/orm/build/make-fields.ts: -------------------------------------------------------------------------------- 1 | import type { SettingsType } from "~/orm/settings/settings-type.ts"; 2 | import type { EntryType } from "~/orm/entry/entry-type.ts"; 3 | import type { Entry } from "~/orm/entry/entry.ts"; 4 | import type { Settings } from "~/orm/settings/settings.ts"; 5 | import { raiseORMException } from "~/orm/orm-exception.ts"; 6 | import type { 7 | ChildEntry, 8 | ChildEntryType, 9 | } from "~/orm/child-entry/child-entry.ts"; 10 | 11 | interface ForTypeMap { 12 | entry: EntryType; 13 | settings: SettingsType; 14 | child: ChildEntryType; 15 | } 16 | 17 | interface ForTypeMapWithData { 18 | entry: typeof Entry; 19 | settings: typeof Settings; 20 | child: typeof ChildEntry; 21 | } 22 | export function makeFields( 23 | forType: ForType, 24 | typeClass: ForTypeMap[ForType], 25 | dataClass: ForTypeMapWithData[ForType], 26 | ): void { 27 | const fields = typeClass.fields; 28 | const children = typeClass.children || []; 29 | for (const childName of children.keys()) { 30 | Object.defineProperty(dataClass.prototype, childName, { 31 | get(): any { 32 | return (this as Entry | Settings).getChild(childName as string); 33 | }, 34 | enumerable: true, 35 | }); 36 | } 37 | for (const field of fields.values()) { 38 | Object.defineProperty(dataClass.prototype, field.key, { 39 | enumerable: true, 40 | get(): any { 41 | if (!(this as Entry | Settings)._data.has(field.key)) { 42 | (this as Entry | Settings)._data.set(field.key, null); 43 | } 44 | return (this as Entry)._data.get(field.key); 45 | }, 46 | set(value): void { 47 | const instance = this as Entry | Settings | ChildEntry; 48 | const fieldType = instance._getFieldType(field.type); 49 | const fieldDef = instance._getFieldDef(field.key); 50 | value = fieldType.normalize(value, fieldDef); 51 | const existingValue = instance._data.get(field.key); 52 | if (existingValue === value) { 53 | return; 54 | } 55 | instance._modifiedValues.set(field.key, { 56 | from: existingValue, 57 | to: value, 58 | }); 59 | const isValid = fieldType.validate(value, fieldDef); 60 | if (!isValid) { 61 | raiseORMException( 62 | `${value} is not a valid value for field ${field.key} in ${forType} ${instance._name}`, 63 | ); 64 | } 65 | 66 | instance._data.set(field.key, value); 67 | }, 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/orm/db/postgres/maps/maps.ts: -------------------------------------------------------------------------------- 1 | import type { DataTypeMap, ServerStatus } from "~/orm/db/postgres/pgTypes.ts"; 2 | 3 | export const dataTypeMap: DataTypeMap = { 4 | 16: "bool", 5 | 17: "bytea", 6 | 18: "char", 7 | 19: "name", 8 | 20: "int8", 9 | 21: "int2", 10 | 22: "int2vector", 11 | 23: "int4", 12 | 24: "regproc", 13 | 25: "text", 14 | 26: "oid", 15 | 27: "tid", 16 | 28: "xid", 17 | 29: "cid", 18 | 30: "oidvector", 19 | 114: "json", 20 | 142: "xml", 21 | 1042: "bpchar", 22 | 1043: "varchar", 23 | 1114: "timestamp", 24 | 1184: "timestamptz", 25 | 1082: "date", 26 | 1083: "time", 27 | 1700: "numeric", 28 | 3802: "jsonb", 29 | }; 30 | 31 | export const statusMap: Record = { 32 | "I": "idle", 33 | "T": "transaction", 34 | "E": "error", 35 | "K": "keyData", 36 | }; 37 | export function getDataType( 38 | dataTypeID: number, 39 | ): DataTypeMap[keyof DataTypeMap] | "unknown" { 40 | const id = dataTypeID as keyof DataTypeMap; 41 | 42 | if (dataTypeMap[id]) { 43 | return dataTypeMap[id]; 44 | } 45 | return "unknown"; 46 | } 47 | 48 | function _stripNulls(data: Uint8Array) { 49 | let i = data.length - 1; 50 | while (data[i] === 0) { 51 | i--; 52 | } 53 | return data.slice(0, i + 1); 54 | } 55 | function decodeText(data: Uint8Array) { 56 | return new TextDecoder().decode(data); 57 | } 58 | export function convertToDataType( 59 | data: Uint8Array, 60 | _type: number, 61 | dataType: DataTypeMap[keyof DataTypeMap] | "unknown", 62 | ): any { 63 | // data = stripNulls(data); 64 | const text = decodeText(data); 65 | 66 | switch (dataType) { 67 | case "bool": 68 | return text === "t"; 69 | case "int2": 70 | return parseInt(text); 71 | case "int4": 72 | return parseInt(text); 73 | case "int8": 74 | return parseInt(text); 75 | case "xml": 76 | return JSON.parse(text); 77 | case "timestamptz": 78 | return new Date(text).getTime(); 79 | case "timestamp": 80 | return new Date(text).getTime(); 81 | case "varchar": 82 | return text; 83 | case "text": 84 | return text; 85 | case "date": 86 | return text; 87 | case "numeric": 88 | return parseFloat(text); 89 | case "jsonb": 90 | return JSON.parse(text); 91 | case "json": 92 | return JSON.parse(text); 93 | case "bpchar": 94 | return text; 95 | case "time": 96 | return text; 97 | default: 98 | return text; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/orm/db/postgres/pgError.ts: -------------------------------------------------------------------------------- 1 | import ColorMe from "../../../terminal/color-me.ts"; 2 | import { convertString } from "../../../utils/mod.ts"; 3 | import { PGErrorCode } from "./maps/errorMap.ts"; 4 | 5 | export class PgError extends Error { 6 | severity: string; 7 | code: string; 8 | detail: string; 9 | override message: string; 10 | fullMessage: Record; 11 | query?: string; 12 | 13 | constructor(options: Record) { 14 | super(options.message); 15 | this.severity = options.severity; 16 | this.code = options.code; 17 | this.detail = options.detail; 18 | this.message = options.message; 19 | this.name = options.name; 20 | this.fullMessage = options; 21 | this.query = options.query; 22 | } 23 | } 24 | 25 | export function isPgError(error: unknown): error is PgError { 26 | return error instanceof PgError; 27 | } 28 | 29 | export function handlePgError(error: PgError) { 30 | const response: Array = []; 31 | const subject = error.name; 32 | switch (error.code) { 33 | case PGErrorCode.UndefinedColumn: 34 | response.push(`${ 35 | convertString( 36 | error.message.split('"')[1], 37 | "camel", 38 | ) 39 | } field does not exist in the database. You may need to run a migration`); 40 | break; 41 | case PGErrorCode.ForeignKeyViolation: 42 | response.push(error.detail); 43 | 44 | break; 45 | case PGErrorCode.InvalidCatalogName: 46 | response.push( 47 | `Database ${error.message} does not exist. Please check your database configuration`, 48 | ); 49 | break; 50 | case PGErrorCode.SyntaxError: 51 | if (error.fullMessage.position && error.query) { 52 | const position = parseInt(error.fullMessage.position, 10); 53 | const query = error.query; 54 | const firstPart = query.slice(0, position - 1); 55 | let secondPart = query.slice(position - 1); 56 | let match = error.message.replace('syntax error at or near "', ""); 57 | match = match.substring(0, match.length - 1); 58 | secondPart = secondPart.replace( 59 | match, 60 | ColorMe.fromOptions(match, { color: "brightRed" }), 61 | ); 62 | response.push(`${firstPart}${secondPart}`); 63 | break; 64 | } 65 | 66 | response.push( 67 | `Query: ${error.query} \n ${ 68 | ColorMe.fromOptions(error.message, { color: "brightRed" }) 69 | }`, 70 | ); 71 | break; 72 | default: 73 | console.log(error); 74 | throw error; 75 | } 76 | return { subject, response }; 77 | } 78 | -------------------------------------------------------------------------------- /src/orm/db/postgres/in-pg/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const ERRNO_CODES = { 2 | EPERM: 63, 3 | ENOENT: 44, 4 | ESRCH: 71, 5 | EINTR: 27, 6 | EIO: 29, 7 | ENXIO: 60, 8 | E2BIG: 1, 9 | ENOEXEC: 45, 10 | EBADF: 8, 11 | ECHILD: 12, 12 | EAGAIN: 6, 13 | EWOULDBLOCK: 6, 14 | ENOMEM: 48, 15 | EACCES: 2, 16 | EFAULT: 21, 17 | ENOTBLK: 105, 18 | EBUSY: 10, 19 | EEXIST: 20, 20 | EXDEV: 75, 21 | ENODEV: 43, 22 | ENOTDIR: 54, 23 | EISDIR: 31, 24 | EINVAL: 28, 25 | ENFILE: 41, 26 | EMFILE: 33, 27 | ENOTTY: 59, 28 | ETXTBSY: 74, 29 | EFBIG: 22, 30 | ENOSPC: 51, 31 | ESPIPE: 70, 32 | EROFS: 69, 33 | EMLINK: 34, 34 | EPIPE: 64, 35 | EDOM: 18, 36 | ERANGE: 68, 37 | ENOMSG: 49, 38 | EIDRM: 24, 39 | ECHRNG: 106, 40 | EL2NSYNC: 156, 41 | EL3HLT: 107, 42 | EL3RST: 108, 43 | ELNRNG: 109, 44 | EUNATCH: 110, 45 | ENOCSI: 111, 46 | EL2HLT: 112, 47 | EDEADLK: 16, 48 | ENOLCK: 46, 49 | EBADE: 113, 50 | EBADR: 114, 51 | EXFULL: 115, 52 | ENOANO: 104, 53 | EBADRQC: 103, 54 | EBADSLT: 102, 55 | EDEADLOCK: 16, 56 | EBFONT: 101, 57 | ENOSTR: 100, 58 | ENODATA: 116, 59 | ETIME: 117, 60 | ENOSR: 118, 61 | ENONET: 119, 62 | ENOPKG: 120, 63 | EREMOTE: 121, 64 | ENOLINK: 47, 65 | EADV: 122, 66 | ESRMNT: 123, 67 | ECOMM: 124, 68 | EPROTO: 65, 69 | EMULTIHOP: 36, 70 | EDOTDOT: 125, 71 | EBADMSG: 9, 72 | ENOTUNIQ: 126, 73 | EBADFD: 127, 74 | EREMCHG: 128, 75 | ELIBACC: 129, 76 | ELIBBAD: 130, 77 | ELIBSCN: 131, 78 | ELIBMAX: 132, 79 | ELIBEXEC: 133, 80 | ENOSYS: 52, 81 | ENOTEMPTY: 55, 82 | ENAMETOOLONG: 37, 83 | ELOOP: 32, 84 | EOPNOTSUPP: 138, 85 | EPFNOSUPPORT: 139, 86 | ECONNRESET: 15, 87 | ENOBUFS: 42, 88 | EAFNOSUPPORT: 5, 89 | EPROTOTYPE: 67, 90 | ENOTSOCK: 57, 91 | ENOPROTOOPT: 50, 92 | ESHUTDOWN: 140, 93 | ECONNREFUSED: 14, 94 | EADDRINUSE: 3, 95 | ECONNABORTED: 13, 96 | ENETUNREACH: 40, 97 | ENETDOWN: 38, 98 | ETIMEDOUT: 73, 99 | EHOSTDOWN: 142, 100 | EHOSTUNREACH: 23, 101 | EINPROGRESS: 26, 102 | EALREADY: 7, 103 | EDESTADDRREQ: 17, 104 | EMSGSIZE: 35, 105 | EPROTONOSUPPORT: 66, 106 | ESOCKTNOSUPPORT: 137, 107 | EADDRNOTAVAIL: 4, 108 | ENETRESET: 39, 109 | EISCONN: 30, 110 | ENOTCONN: 53, 111 | ETOOMANYREFS: 141, 112 | EUSERS: 136, 113 | EDQUOT: 19, 114 | ESTALE: 72, 115 | ENOTSUP: 138, 116 | ENOMEDIUM: 148, 117 | EILSEQ: 25, 118 | EOVERFLOW: 61, 119 | ECANCELED: 11, 120 | ENOTRECOVERABLE: 56, 121 | EOWNERDEAD: 62, 122 | ESTRPIPE: 135, 123 | }; 124 | 125 | export const IOCTL = { 126 | TIOCGWINSZ: 21523, 127 | }; 128 | -------------------------------------------------------------------------------- /src/in-live/broker-client.ts: -------------------------------------------------------------------------------- 1 | export class BrokerClient { 2 | channel: string; 3 | #stop: boolean = false; 4 | #socket: WebSocket | null = null; 5 | onMessage: (message: T) => void = () => { 6 | console.log("Default onMessage handler called"); 7 | }; 8 | port: number = 11254; 9 | 10 | constructor(channel: string) { 11 | this.channel = channel; 12 | } 13 | get closed(): boolean { 14 | if (!this.#socket) { 15 | return true; 16 | } 17 | 18 | return this.#socket.readyState === WebSocket.CLOSED; 19 | } 20 | get connected(): boolean { 21 | return this.#socket?.readyState === WebSocket.OPEN; 22 | } 23 | get closing(): boolean { 24 | return this.#socket?.readyState === WebSocket.CLOSING; 25 | } 26 | 27 | get connecting(): boolean { 28 | return this.#socket?.readyState === WebSocket.CONNECTING; 29 | } 30 | 31 | get connectionStatus(): ConnectionStatus { 32 | const status = this.#socket?.readyState; 33 | switch (status) { 34 | case WebSocket.CONNECTING: 35 | return "CONNECTING"; 36 | case WebSocket.OPEN: 37 | return "OPEN"; 38 | case WebSocket.CLOSING: 39 | return "CLOSING"; 40 | case WebSocket.CLOSED: 41 | return "CLOSED"; 42 | default: 43 | return "UNKNOWN"; 44 | } 45 | } 46 | connect(brokerPort: number) { 47 | this.port = brokerPort; 48 | this.#reconnect(); 49 | } 50 | stop() { 51 | this.#stop = true; 52 | this.#socket?.close(); 53 | } 54 | async #reconnect() { 55 | while (this.closed) { 56 | if (this.connected || this.#stop) { 57 | break; 58 | } 59 | await this.#connect(); 60 | await new Promise((resolve) => setTimeout(resolve, 1000)); 61 | } 62 | } 63 | #connect() { 64 | this.#socket = new WebSocket(`ws://127.0.0.1:${this.port}/ws`); 65 | return new Promise((resolve) => { 66 | setTimeout(() => { 67 | resolve(); 68 | }, 2000); 69 | this.#socket!.onopen = () => { 70 | this.#socket!.onmessage = (event) => { 71 | const data = JSON.parse(event.data) as T; 72 | 73 | this.onMessage(data); 74 | }; 75 | this.#socket!.onclose = () => { 76 | this.#reconnect(); 77 | }; 78 | resolve(); 79 | }; 80 | }); 81 | } 82 | onMessageReceived(callback: (message: T) => void) { 83 | this.onMessage = callback; 84 | } 85 | broadcast(message: T) { 86 | if (!this.connected) { 87 | return; 88 | } 89 | this.#socket!.send(JSON.stringify(message)); 90 | } 91 | } 92 | 93 | type ConnectionStatus = 94 | | "CONNECTING" 95 | | "OPEN" 96 | | "CLOSING" 97 | | "CLOSED" 98 | | "UNKNOWN"; 99 | -------------------------------------------------------------------------------- /src/in-queue/in-queue-client.ts: -------------------------------------------------------------------------------- 1 | import { inLog } from "#inLog"; 2 | import type { ConnectionStatus, TaskInfo } from "./types.ts"; 3 | 4 | export class InQueueClient { 5 | #stop: boolean = false; 6 | #socket: WebSocket | null = null; 7 | onMessage: (message: Record) => void = () => { 8 | console.log("Default onMessage handler called"); 9 | }; 10 | port: number = 11354; 11 | 12 | constructor() {} 13 | init(queuePort: number) { 14 | this.connect(queuePort); 15 | } 16 | get closed(): boolean { 17 | if (!this.#socket) { 18 | return true; 19 | } 20 | 21 | return this.#socket.readyState === WebSocket.CLOSED; 22 | } 23 | get connected(): boolean { 24 | return this.#socket?.readyState === WebSocket.OPEN; 25 | } 26 | get closing(): boolean { 27 | return this.#socket?.readyState === WebSocket.CLOSING; 28 | } 29 | 30 | get connecting(): boolean { 31 | return this.#socket?.readyState === WebSocket.CONNECTING; 32 | } 33 | 34 | get connectionStatus(): ConnectionStatus { 35 | const status = this.#socket?.readyState; 36 | switch (status) { 37 | case WebSocket.CONNECTING: 38 | return "CONNECTING"; 39 | case WebSocket.OPEN: 40 | return "OPEN"; 41 | case WebSocket.CLOSING: 42 | return "CLOSING"; 43 | case WebSocket.CLOSED: 44 | return "CLOSED"; 45 | default: 46 | return "UNKNOWN"; 47 | } 48 | } 49 | connect(queuePort: number) { 50 | this.port = queuePort; 51 | this.#reconnect(); 52 | } 53 | stop() { 54 | this.#stop = true; 55 | this.#socket?.close(); 56 | } 57 | async #reconnect() { 58 | while (this.closed) { 59 | if (this.connected || this.#stop) { 60 | break; 61 | } 62 | await this.#connect(); 63 | await new Promise((resolve) => setTimeout(resolve, 1000)); 64 | } 65 | } 66 | #connect() { 67 | this.#socket = new WebSocket(`ws://127.0.0.1:${this.port}/ws`); 68 | return new Promise((resolve) => { 69 | setTimeout(() => { 70 | resolve(); 71 | }, 2000); 72 | this.#socket!.onopen = () => { 73 | this.#socket!.onmessage = (event) => { 74 | const data = JSON.parse(event.data) as Record; 75 | 76 | this.onMessage(data); 77 | }; 78 | this.#socket!.onclose = () => { 79 | this.#reconnect(); 80 | }; 81 | resolve(); 82 | }; 83 | }); 84 | } 85 | onMessageReceived(callback: (message: Record) => void) { 86 | this.onMessage = callback; 87 | } 88 | send(message: TaskInfo) { 89 | if (!this.connected) { 90 | inLog.debug("Queue is not connected"); 91 | return; 92 | } 93 | this.#socket!.send(JSON.stringify(message)); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/auth/entries/account/_account.type.ts: -------------------------------------------------------------------------------- 1 | import type { ChildList, EntryBase } from "@inspatial/cloud/types"; 2 | 3 | export interface Account extends EntryBase { 4 | _name: "account"; 5 | /** 6 | * **Onboarding Complete** (BooleanField) 7 | * @type {boolean} 8 | */ 9 | onboardingComplete?: boolean; 10 | /** 11 | * **Initialized** (BooleanField) 12 | * @type {boolean} 13 | */ 14 | initialized?: boolean; 15 | /** 16 | * **Onboarding Response** (JSONField) 17 | * @type {Record} 18 | */ 19 | obResponse?: Record; 20 | /** 21 | * **Account** (IDField) 22 | * @type {string} 23 | * @required true 24 | */ 25 | id: string; 26 | /** 27 | * **Created At** (TimeStampField) 28 | * @description The date and time this entry was created 29 | * @type {number} 30 | * @required true 31 | */ 32 | createdAt: number; 33 | /** 34 | * **Updated At** (TimeStampField) 35 | * @description The date and time this entry was last updated 36 | * @type {number} 37 | * @required true 38 | */ 39 | updatedAt: number; 40 | users: ChildList<{ 41 | /** 42 | * **User** (ConnectionField) 43 | * 44 | * **EntryType** `user` 45 | * @type {string} 46 | * @required true 47 | */ 48 | user: string; 49 | /** 50 | * **Role** (ChoicesField) 51 | * @type {'systemAdmin' | 'accountOwner'} 52 | */ 53 | role?: "systemAdmin" | "accountOwner"; 54 | /** 55 | * **ID** (IDField) 56 | * @type {string} 57 | * @required true 58 | */ 59 | id: string; 60 | /** 61 | * **Order** (IntField) 62 | * @description The order of this child in the list 63 | * @type {number} 64 | */ 65 | order?: number; 66 | /** 67 | * **Created At** (TimeStampField) 68 | * @description The date and time this child was created 69 | * @type {number} 70 | * @required true 71 | */ 72 | createdAt: number; 73 | /** 74 | * **Updated At** (TimeStampField) 75 | * @description The date and time this child was last updated 76 | * @type {number} 77 | * @required true 78 | */ 79 | updatedAt: number; 80 | /** 81 | * **Parent** (ConnectionField) 82 | * 83 | * **EntryType** `account` 84 | * @type {string} 85 | * @required true 86 | */ 87 | parent: string; 88 | /** 89 | * **User Title** (DataField) 90 | * @description The user's full name (automatically generated) 91 | * @type {string} 92 | */ 93 | user__title?: string; 94 | }>; 95 | runAction( 96 | actionName: N, 97 | ): AccountActionMap[N]["return"]; 98 | } 99 | type AccountActionMap = { 100 | initialize: { 101 | return: Promise; 102 | }; 103 | }; 104 | --------------------------------------------------------------------------------