foobara-typescript-remote-command-generator 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +7 -0
  3. data/LICENSE-APACHE.txt +202 -0
  4. data/LICENSE-MIT.txt +21 -0
  5. data/LICENSE.txt +8 -0
  6. data/README.md +55 -0
  7. data/lib/foobara/typescript_remote_command_generator.rb +13 -0
  8. data/src/remote_generator/association_depth.rb +11 -0
  9. data/src/remote_generator/generate_typescript.rb +85 -0
  10. data/src/remote_generator/services/aggregate_entity_generator.rb +49 -0
  11. data/src/remote_generator/services/aggregate_model_generator.rb +53 -0
  12. data/src/remote_generator/services/atom_entity_generator.rb +47 -0
  13. data/src/remote_generator/services/atom_model_generator.rb +49 -0
  14. data/src/remote_generator/services/command_errors_generator.rb +36 -0
  15. data/src/remote_generator/services/command_errors_index_generator.rb +31 -0
  16. data/src/remote_generator/services/command_generator.rb +41 -0
  17. data/src/remote_generator/services/command_inputs_generator.rb +33 -0
  18. data/src/remote_generator/services/command_manifest_generator.rb +13 -0
  19. data/src/remote_generator/services/command_result_generator.rb +80 -0
  20. data/src/remote_generator/services/dependency_group.rb +137 -0
  21. data/src/remote_generator/services/domain_config_generator.rb +21 -0
  22. data/src/remote_generator/services/domain_generator.rb +63 -0
  23. data/src/remote_generator/services/domain_manifest_generator.rb +17 -0
  24. data/src/remote_generator/services/entity_generator.rb +43 -0
  25. data/src/remote_generator/services/entity_manifest_generator.rb +10 -0
  26. data/src/remote_generator/services/entity_variants_generator.rb +47 -0
  27. data/src/remote_generator/services/error_generator.rb +68 -0
  28. data/src/remote_generator/services/loaded_entity_generator.rb +27 -0
  29. data/src/remote_generator/services/manifest_generator.rb +13 -0
  30. data/src/remote_generator/services/model_generator.rb +84 -0
  31. data/src/remote_generator/services/model_manifest_generator.rb +11 -0
  32. data/src/remote_generator/services/model_variants_generator.rb +35 -0
  33. data/src/remote_generator/services/organization_config_generator.rb +21 -0
  34. data/src/remote_generator/services/organization_generator.rb +41 -0
  35. data/src/remote_generator/services/organization_manifest_generator.rb +17 -0
  36. data/src/remote_generator/services/processor_class_generator.rb +18 -0
  37. data/src/remote_generator/services/root_manifest_generator.rb +13 -0
  38. data/src/remote_generator/services/typescript_from_manifest_base_generator.rb +293 -0
  39. data/src/remote_generator/services/unloaded_entity_generator.rb +25 -0
  40. data/src/remote_generator/write_typescript_to_disk.rb +78 -0
  41. data/src/remote_generator.rb +5 -0
  42. data/templates/Command/Errors.ts.erb +11 -0
  43. data/templates/Command/Inputs.ts.erb +5 -0
  44. data/templates/Command/Result.ts.erb +7 -0
  45. data/templates/Command/errors/index.ts.erb +3 -0
  46. data/templates/Command.ts.erb +11 -0
  47. data/templates/Domain/config.ts.erb +16 -0
  48. data/templates/Domain.ts.erb +17 -0
  49. data/templates/Entity/Aggregate.ts.erb +18 -0
  50. data/templates/Entity/Ambiguous.ts.erb +30 -0
  51. data/templates/Entity/Atom.ts.erb +18 -0
  52. data/templates/Entity/Loaded.ts.erb +14 -0
  53. data/templates/Entity/Unloaded.ts.erb +18 -0
  54. data/templates/EntityVariants.ts.erb +40 -0
  55. data/templates/Error.ts.erb +8 -0
  56. data/templates/Model/Aggregate.ts.erb +12 -0
  57. data/templates/Model/Atom.ts.erb +12 -0
  58. data/templates/Model/Model.ts.erb +18 -0
  59. data/templates/ModelVariants.ts.erb +26 -0
  60. data/templates/Organization/config.ts.erb +16 -0
  61. data/templates/Organization.ts.erb +8 -0
  62. data/templates/base/DataPath.ts +49 -0
  63. data/templates/base/Entity.ts +87 -0
  64. data/templates/base/Error.ts +29 -0
  65. data/templates/base/Model.ts +24 -0
  66. data/templates/base/Outcome.ts +42 -0
  67. data/templates/base/RemoteCommand.ts +87 -0
  68. metadata +142 -0
@@ -0,0 +1,11 @@
1
+ import RemoteCommand from "<%= path_to_root %>base/RemoteCommand"
2
+
3
+ import Inputs from "./Inputs"
4
+ import Result from "./Result"
5
+ import { Error } from "./Errors"
6
+
7
+ export class <%= command_name %> extends RemoteCommand<Inputs, Result, Error> {
8
+ static readonly organizationName = "<%= organization_name %>"
9
+ static readonly domainName = "<%= domain_name %>"
10
+ static readonly commandName = "<%= command_name %>"
11
+ }
@@ -0,0 +1,16 @@
1
+ let _urlBase = process.env.REACT_APP_FOOBARA_GLOBAL_URL_BASE
2
+
3
+ const config = {
4
+ get urlBase(): string {
5
+ if (_urlBase === undefined) {
6
+ throw new Error("urlBase is not set and REACT_APP_FOOBARA_GLOBAL_URL_BASE is undefined")
7
+ }
8
+
9
+ return _urlBase
10
+ },
11
+ set urlBase(urlBase: string) {
12
+ _urlBase = urlBase
13
+ }
14
+ }
15
+
16
+ export default config
@@ -0,0 +1,17 @@
1
+ export * as config from "./config"
2
+
3
+ export const isGlobal = <%= global? %>
4
+ export const organizationName = "<%= organization_name %>"
5
+ export const domainName = "<%= domain_name %>"
6
+
7
+ <% command_generators.each do |command| %>
8
+ export <%= command.import_destructure %> from "<%= path_to_root %><%= command.import_path %>"
9
+ <% if command.command_errors_index_generator.applicable? %>
10
+ export <%= command.command_errors_index_generator.import_destructure %> from "<%= path_to_root %><%= command.command_errors_index_generator.import_path %>"
11
+ <% end %>
12
+ <% end %>
13
+
14
+ // TODO: put these on an entities module so that commands can be the only top-level interface.
15
+ <% entity_generators.each do |entity| %>
16
+ export <%= entity.import_destructure %> from "<%= path_to_root %><%= entity.import_path %>"
17
+ <% end %>
@@ -0,0 +1,18 @@
1
+ import {
2
+ <%= entity_name %>AttributesType
3
+ } from "./Ambiguous"
4
+ import { Loaded<%= entity_name %> } from "./Loaded"
5
+ <% dependency_roots.each do |dependency_root| %>
6
+ import { <%= dependency_root.ts_instance_name %> } from "<%= path_to_root %><%= dependency_root.import_path %>"
7
+ <% end %>
8
+
9
+ export interface <%= entity_name %>AggregateAttributesType extends <%= entity_name %>AttributesType <%= attributes_type_ts_type %>
10
+
11
+ export class <%= entity_name %>Aggregate extends Loaded<%= entity_name %><<%= entity_name %>AggregateAttributesType> {
12
+ <% if has_associations? %>
13
+ /* eslint-disable @typescript-eslint/class-literal-property-style */
14
+ get isAtom (): false { return false }
15
+ get isAggregate (): true { return true }
16
+ /* eslint-enable @typescript-eslint/class-literal-property-style */
17
+ <% end %>
18
+ }
@@ -0,0 +1,30 @@
1
+ import { Entity } from "<%= path_to_root %>base/Entity"
2
+ <% dependency_roots.each do |dependency_root| %>
3
+ import { <%= dependency_root.scoped_name %> } from "<%= path_to_root %><%= dependency_root.import_path %>"
4
+ <% end %>
5
+
6
+ export type <%= entity_name %>PrimaryKeyType = <%= primary_key_ts_type %>
7
+ export const <%= entity_name_downcase %>PrimaryKeyAttributeName: "<%= primary_key_name %>" = "<%= primary_key_name %>"
8
+ export interface <%= entity_name %>AttributesType <%= attributes_type_ts_type %>
9
+
10
+ export class <%= entity_name %><
11
+ AttributesType extends <%= entity_name %>AttributesType = <%= entity_name %>AttributesType
12
+ > extends Entity<<%= entity_name %>PrimaryKeyType, AttributesType> {
13
+ static readonly modelName: string = "<%= entity_name %>"
14
+ static readonly entityName: string = "<%= entity_name %>"
15
+ static readonly primaryKeyAttributeName: "<%= primary_key_name %>" = "<%= primary_key_name %>"
16
+
17
+ get <%= primary_key_name %> (): <%= entity_name %>PrimaryKeyType {
18
+ return this.primaryKey
19
+ }
20
+
21
+ get associationPropertyPaths (): string[][] { return <%= association_property_paths_ts %> }
22
+ readonly hasAssociations: <%= has_associations? %> = <%= has_associations? %>
23
+
24
+ <% attribute_names.each do |attribute_name| %>
25
+ get <%= attribute_name %> (): AttributesType["<%= attribute_name %>"] {
26
+ return this.readAttribute("<%= attribute_name %>")
27
+ }
28
+ <% end %>
29
+
30
+ }
@@ -0,0 +1,18 @@
1
+ import {
2
+ <%= entity_name %>AttributesType
3
+ } from "./Ambiguous"
4
+ import { Loaded<%= entity_name %> } from "./Loaded"
5
+ <% dependency_roots.each do |dependency_root| %>
6
+ import { <%= dependency_root.ts_instance_name %> } from "<%= path_to_root %><%= dependency_root.import_path %>"
7
+ <% end %>
8
+
9
+ export interface <%= entity_name %>AtomAttributesType extends <%= entity_name %>AttributesType <%= atom_attributes_ts_type %>
10
+
11
+ export class <%= entity_name %>Atom extends Loaded<%= entity_name %><<%= entity_name %>AtomAttributesType> {
12
+ <% if has_associations? %>
13
+ /* eslint-disable @typescript-eslint/class-literal-property-style */
14
+ get isAtom (): true { return true }
15
+ get isAggregate (): false { return false }
16
+ /* eslint-enable @typescript-eslint/class-literal-property-style */
17
+ <% end %>
18
+ }
@@ -0,0 +1,14 @@
1
+ import {
2
+ <%= entity_name %>,
3
+ <%= entity_name %>AttributesType
4
+ } from "./Ambiguous"
5
+
6
+ export class Loaded<%= entity_name %><T extends <%= entity_name %>AttributesType = <%= entity_name %>AttributesType> extends <%= entity_name %><T> {
7
+ readonly isLoaded: true = true
8
+ <% unless has_associations? %>
9
+ /* eslint-disable @typescript-eslint/class-literal-property-style */
10
+ get isAtom (): true { return true }
11
+ get isAggregate (): true { return true }
12
+ /* eslint-enable @typescript-eslint/class-literal-property-style */
13
+ <% end %>
14
+ }
@@ -0,0 +1,18 @@
1
+ import { Never } from "<%= path_to_root %>base/Entity"
2
+ import {
3
+ <%= entity_name %>,
4
+ //<%= entity_name %>PrimaryKeyType
5
+ <%= entity_name %>AttributesType
6
+ } from "./Ambiguous"
7
+
8
+ export type Unloaded<%= entity_name %>AttributesType = Never<<%= entity_name %>AttributesType>
9
+
10
+ export class Unloaded<%= entity_name %> extends <%= entity_name %><Unloaded<%= entity_name %>AttributesType> {
11
+ /*
12
+ constructor(id: <%= entity_name %>PrimaryKeyType) {
13
+ super(id, {})
14
+ }
15
+ */
16
+
17
+ readonly isLoaded: false = false
18
+ }
@@ -0,0 +1,40 @@
1
+ import {
2
+ <%= entity_name %>,
3
+ <%= entity_name %>PrimaryKeyType,
4
+ <%= entity_name_downcase %>PrimaryKeyAttributeName,
5
+ <%= entity_name %>AttributesType
6
+ } from "./<%= entity_name %>/Ambiguous"
7
+ import {
8
+ Unloaded<%= entity_name %>,
9
+ Unloaded<%= entity_name %>AttributesType
10
+ } from "./<%= entity_name %>/Unloaded"
11
+ import {
12
+ Loaded<%= entity_name %>
13
+ } from "./<%= entity_name %>/Loaded"
14
+
15
+ <% if has_associations? %>
16
+ import {
17
+ <%= entity_name %>Atom,
18
+ <%= entity_name %>AtomAttributesType
19
+ } from "./<%= entity_name %>/Atom"
20
+ import {
21
+ <%= entity_name %>Aggregate,
22
+ <%= entity_name %>AggregateAttributesType
23
+ } from "./<%= entity_name %>/Aggregate"
24
+ <% end %>
25
+
26
+ export {
27
+ <%= entity_name %>,
28
+ type <%= entity_name %>AttributesType,
29
+ Unloaded<%= entity_name %>,
30
+ type Unloaded<%= entity_name %>AttributesType,
31
+ Loaded<%= entity_name %>,
32
+ <% if has_associations? %>
33
+ <%= entity_name %>Atom,
34
+ type <%= entity_name %>AtomAttributesType,
35
+ <%= entity_name %>Aggregate,
36
+ type <%= entity_name %>AggregateAttributesType,
37
+ <% end %>
38
+ type <%= entity_name %>PrimaryKeyType,
39
+ <%= entity_name_downcase %>PrimaryKeyAttributeName
40
+ }
@@ -0,0 +1,8 @@
1
+ <% dependency_roots.each do |dependency_root| %>
2
+ import { <%= dependency_root.scoped_name %> } from "<%= path_to_root %><%= dependency_root.import_path %>"
3
+ <% end %>
4
+
5
+ import { <%= error_base_class %> } from "<%= path_to_root %>base/Error"
6
+
7
+ export class <%= error_name %> extends <%= error_base_class %><<%= context_ts_type %>> {
8
+ }
@@ -0,0 +1,12 @@
1
+ import {
2
+ <%= model_name %>,
3
+ <%= model_name %>AttributesType
4
+ } from "./<%= model_name %>"
5
+ <% dependency_roots.each do |dependency_root| %>
6
+ import { <%= dependency_root.ts_instance_name %> } from "<%= path_to_root %><%= dependency_root.import_path %>"
7
+ <% end %>
8
+
9
+ export interface <%= model_name %>AggregateAttributesType extends <%= model_name %>AttributesType <%= attributes_type_ts_type %>
10
+
11
+ export class <%= model_name %>Aggregate extends <%= model_name %><<%= model_name %>AggregateAttributesType> {
12
+ }
@@ -0,0 +1,12 @@
1
+ import {
2
+ <%= model_name %>,
3
+ <%= model_name %>AttributesType
4
+ } from "./<%= model_name %>"
5
+ <% dependency_roots.each do |dependency_root| %>
6
+ import { <%= dependency_root.ts_instance_name %> } from "<%= path_to_root %><%= dependency_root.import_path %>"
7
+ <% end %>
8
+
9
+ export interface <%= model_name %>AtomAttributesType extends <%= model_name %>AttributesType <%= atom_attributes_ts_type %>
10
+
11
+ export class <%= model_name %>Atom extends <%= model_name %><<%= model_name %>AtomAttributesType> {
12
+ }
@@ -0,0 +1,18 @@
1
+ import { Model } from "<%= path_to_root %>base/Model"
2
+ <% dependency_roots.each do |dependency_root| %>
3
+ import { <%= dependency_root.scoped_name %> } from "<%= path_to_root %><%= dependency_root.import_path %>"
4
+ <% end %>
5
+
6
+ export interface <%= model_name %>AttributesType <%= attributes_type_ts_type %>
7
+
8
+ export class <%= model_name %><
9
+ AttributesType extends <%= model_name %>AttributesType = <%= model_name %>AttributesType
10
+ > extends Model<AttributesType> {
11
+ static readonly modelName: string = "<%= model_name %>"
12
+
13
+ <% attribute_names.each do |attribute_name| %>
14
+ get <%= attribute_name %> (): AttributesType["<%= attribute_name %>"] {
15
+ return this.readAttribute("<%= attribute_name %>")
16
+ }
17
+ <% end %>
18
+ }
@@ -0,0 +1,26 @@
1
+ import {
2
+ <%= model_name %>,
3
+ <%= model_name %>AttributesType
4
+ } from "./<%= model_name %>/<%= model_name %>"
5
+
6
+ <% if has_associations? %>
7
+ import {
8
+ <%= model_name %>Atom,
9
+ <%= model_name %>AtomAttributesType
10
+ } from "./<%= model_name %>/Atom"
11
+ import {
12
+ <%= model_name %>Aggregate,
13
+ <%= model_name %>AggregateAttributesType
14
+ } from "./<%= model_name %>/Aggregate"
15
+ <% end %>
16
+
17
+ export {
18
+ <%= model_name %>,
19
+ type <%= model_name %>AttributesType,
20
+ <% if has_associations? %>
21
+ <%= model_name %>Atom,
22
+ type <%= model_name %>AtomAttributesType,
23
+ <%= model_name %>Aggregate,
24
+ type <%= model_name %>AggregateAttributesType,
25
+ <% end %>
26
+ }
@@ -0,0 +1,16 @@
1
+ let _urlBase = process.env.REACT_APP_FOOBARA_GLOBAL_URL_BASE
2
+
3
+ const config = {
4
+ get urlBase(): string {
5
+ if (_urlBase === undefined) {
6
+ throw new Error("urlBase is not set and REACT_APP_FOOBARA_GLOBAL_URL_BASE is undefined")
7
+ }
8
+
9
+ return _urlBase
10
+ },
11
+ set urlBase(urlBase: string) {
12
+ _urlBase = urlBase
13
+ }
14
+ }
15
+
16
+ export default config
@@ -0,0 +1,8 @@
1
+ export * as config from "./config"
2
+
3
+ <% domain_generators.each do |domain| %>
4
+ export <%= domain.import_destructure %> from "<%= path_to_root %><%= domain.import_path %>"
5
+ <% end %>
6
+
7
+ export const isGlobal = <%= global? %>
8
+ export const organizationName = "<%= organization_name %>"
@@ -0,0 +1,49 @@
1
+ function flatten (array: any[][]): any[] {
2
+ return array.reduce((acc, val) => acc.concat(val), [])
3
+ }
4
+
5
+ function uniq<T> (array: T[]): T[] {
6
+ return Array.from(new Set(array));
7
+ }
8
+
9
+ function compact (array: any[]): any[] {
10
+ return array.filter(item => item !== null && item !== undefined)
11
+ }
12
+
13
+ function _valuesAt<T extends (Record<string, any> | any[])> (objects: T[], path: Array<string | number>): any[] {
14
+ if (path.length === 0) return objects
15
+
16
+ const [pathPart, ...remainingParts] = path
17
+
18
+ let newObjects: any[]
19
+
20
+ if (pathPart === '#') {
21
+ const flat = flatten(objects as any[][])
22
+ newObjects = uniq(flat)
23
+ } else if (typeof pathPart === 'number') {
24
+ newObjects = compact(objects.map((object: T) => {
25
+ if (Array.isArray(object)) {
26
+ return object[pathPart]
27
+ } else {
28
+ throw new Error(`Cannot access index ${pathPart} of object because it's not an array`)
29
+ }
30
+ }))
31
+ } else if (typeof pathPart === 'string') {
32
+ newObjects = compact(objects.map((object: T) => {
33
+ if (typeof object === 'object' && object !== null) {
34
+ const record: Record<string, any> = object
35
+ return record[pathPart]
36
+ } else {
37
+ throw new Error(`Bad object and part: ${pathPart}`)
38
+ }
39
+ }))
40
+ } else {
41
+ throw new Error(`Bad path part: ${typeof pathPart}`)
42
+ }
43
+
44
+ return _valuesAt(newObjects, remainingParts)
45
+ }
46
+
47
+ export function valuesAt<T extends Record<string, any> | any[]> (object: T, path: string[]): any[] {
48
+ return _valuesAt([object], path)
49
+ }
@@ -0,0 +1,87 @@
1
+ import { Model } from './Model';
2
+ import { valuesAt } from './DataPath';
3
+
4
+ export type Never<T> = {[P in keyof T]: never};
5
+
6
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
7
+ export type EntityPrimaryKeyType = number | string
8
+
9
+ export abstract class Entity<PrimaryKeyType extends EntityPrimaryKeyType, AttributesType>
10
+ extends Model<AttributesType> {
11
+ static readonly entityName: string
12
+ static readonly primaryKeyAttributeName: string
13
+
14
+ readonly primaryKey: PrimaryKeyType
15
+ readonly isLoaded: boolean
16
+
17
+ abstract get hasAssociations(): boolean
18
+ abstract get associationPropertyPaths (): string[][]
19
+
20
+ constructor(primaryKey: PrimaryKeyType, attributes: AttributesType) {
21
+ super(attributes)
22
+ this.primaryKey = primaryKey
23
+ this.isLoaded = attributes !== undefined
24
+ }
25
+
26
+ /* Can we make this work or not?
27
+ getConstructor(): EntityConstructor<PrimaryKeyType, AttributesType> {
28
+ return this.constructor as EntityConstructor<PrimaryKeyType, AttributesType>;
29
+ }
30
+ */
31
+ entitiesAt(path: string[]): Entity<EntityPrimaryKeyType, Record<string, any>>[] {
32
+ return valuesAt(this, path).filter(item => item !== undefined) as Entity<EntityPrimaryKeyType, Record<string, any>>[]
33
+ }
34
+
35
+ get isAtom(): boolean {
36
+ if (!this.isLoaded) {
37
+ throw new Error("Record is not loaded so can't check if it's an atom")
38
+ }
39
+
40
+ if (!this.hasAssociations) {
41
+ return true
42
+ }
43
+
44
+ for (const path of this.associationPropertyPaths) {
45
+ for (const record of this.entitiesAt(path)) {
46
+ if (record.isLoaded) {
47
+ return false
48
+ }
49
+ }
50
+ }
51
+
52
+ return true
53
+ }
54
+
55
+ get isAggregate(): boolean {
56
+ if (!this.isLoaded) {
57
+ throw new Error("Record is not loaded so can't check if it's an aggregate")
58
+ }
59
+
60
+ if (!this.hasAssociations) {
61
+ return true
62
+ }
63
+
64
+ for (const path of this.associationPropertyPaths) {
65
+ for (const record of this.entitiesAt(path)) {
66
+ if (!record.isLoaded) {
67
+ return false
68
+ }
69
+
70
+ if (!record.isAggregate) {
71
+ return false
72
+ }
73
+ }
74
+ }
75
+
76
+ return true
77
+ }
78
+ get attributes(): AttributesType {
79
+ if (!this.isLoaded) {
80
+ throw new Error(
81
+ `Cannot read attributes because :${this.primaryKey} is not a loaded record`
82
+ )
83
+ }
84
+
85
+ return this._attributes
86
+ }
87
+ }
@@ -0,0 +1,29 @@
1
+ /* eslint-disable @typescript-eslint/naming-convention */
2
+
3
+ export abstract class FoobaraError<contextT = any> {
4
+ static readonly symbol: string
5
+ static readonly category: 'data' | 'runtime'
6
+
7
+ readonly key: string
8
+ readonly path: string[]
9
+ readonly runtime_path: string[]
10
+ readonly message: string
11
+ readonly context: contextT
12
+
13
+ constructor ({ key, path, runtime_path, message, context }:
14
+ { key: string, path?: string[], runtime_path?: string[], message: string, context: contextT }) {
15
+ this.key = key
16
+ this.path = path ?? []
17
+ this.runtime_path = runtime_path ?? []
18
+ this.message = message
19
+ this.context = context
20
+ }
21
+ }
22
+
23
+ export class DataError<contextT extends Record<string, any>> extends FoobaraError<contextT> {
24
+ static readonly category: 'data' = 'data'
25
+ }
26
+
27
+ export class RuntimeError<contextT extends Record<string, any>> extends FoobaraError<contextT> {
28
+ static readonly category: 'runtime' = 'runtime'
29
+ }
@@ -0,0 +1,24 @@
1
+ export abstract class Model<AttributesType> {
2
+ static readonly modelName: string
3
+ readonly _attributes: AttributesType
4
+
5
+
6
+ constructor(attributes: AttributesType) {
7
+ this._attributes = attributes
8
+ }
9
+
10
+
11
+ /* Can we make this work or not?
12
+ getConstructor(): EntityConstructor<PrimaryKeyType, AttributesType> {
13
+ return this.constructor as EntityConstructor<PrimaryKeyType, AttributesType>;
14
+ }
15
+ */
16
+
17
+ get attributes(): AttributesType {
18
+ return this._attributes
19
+ }
20
+
21
+ readAttribute<T extends keyof this["_attributes"]>(attributeName: T): this["_attributes"][T] {
22
+ return (this.attributes as unknown as this["_attributes"])[attributeName]
23
+ }
24
+ }
@@ -0,0 +1,42 @@
1
+ import { type FoobaraError } from './Error'
2
+
3
+ export class Outcome<Result, OutcomeError extends FoobaraError> {
4
+ readonly result?: Result
5
+ readonly errors: OutcomeError[] = []
6
+ readonly _isSuccess: boolean
7
+
8
+ constructor (result?: Result, errors?: OutcomeError[]) {
9
+ this.result = result
10
+ if (errors != null) {
11
+ this.errors = errors
12
+ }
13
+ this._isSuccess = errors == null || errors.length === 0
14
+ }
15
+
16
+ isSuccess (): this is SuccessfulOutcome<Result, OutcomeError> {
17
+ return this._isSuccess
18
+ }
19
+
20
+ get errorMessage (): string {
21
+ return this.errors.map(e => e.message).join(', ')
22
+ }
23
+ }
24
+
25
+ export class SuccessfulOutcome<Result, OutcomeError extends FoobaraError> extends Outcome<Result, OutcomeError> {
26
+ readonly _isSuccess: true = true
27
+ readonly errors: OutcomeError[] = []
28
+ readonly result: Result
29
+
30
+ constructor (result: Result) {
31
+ super(result, [])
32
+ this.result = result
33
+ }
34
+ }
35
+
36
+ export class ErrorOutcome<Result, OutcomeError extends FoobaraError> extends Outcome<Result, OutcomeError> {
37
+ readonly _isSuccess: false = false
38
+
39
+ constructor (errors: OutcomeError[]) {
40
+ super(undefined, errors)
41
+ }
42
+ }
@@ -0,0 +1,87 @@
1
+ import { type Outcome, SuccessfulOutcome, ErrorOutcome } from './Outcome'
2
+ import { type FoobaraError } from './Error'
3
+
4
+ export default abstract class RemoteCommand<Inputs, Result, CommandError extends FoobaraError> {
5
+ static _urlBase: string | undefined
6
+ static commandName: string
7
+ static organizationName: string
8
+ static domainName: string
9
+
10
+ // TODO: make use of domain's config instead of process.env directly.
11
+ static get urlBase (): string {
12
+ let base = this._urlBase
13
+
14
+ if (base == null) {
15
+ base = process.env.REACT_APP_FOOBARA_GLOBAL_URL_BASE
16
+ }
17
+
18
+ if (base == null) {
19
+ throw new Error("urlBase is not set and REACT_APP_FOOBARA_GLOBAL_URL_BASE is undefined")
20
+ }
21
+
22
+ return base
23
+ }
24
+
25
+ static set urlBase (urlBase: string) {
26
+ this._urlBase = urlBase
27
+ }
28
+
29
+ get organizationName (): string {
30
+ return (this.constructor as typeof RemoteCommand<Inputs, Result, CommandError>).organizationName
31
+ }
32
+
33
+ get domainName (): string {
34
+ return (this.constructor as typeof RemoteCommand<Inputs, Result, CommandError>).domainName
35
+ }
36
+
37
+ inputs: Inputs
38
+
39
+ constructor (inputs: Inputs) {
40
+ this.inputs = inputs
41
+ }
42
+
43
+ get commandName (): string {
44
+ return (this.constructor as typeof RemoteCommand<Inputs, Result, CommandError>).commandName
45
+ }
46
+
47
+ get urlBase (): string {
48
+ return (this.constructor as typeof RemoteCommand<Inputs, Result, CommandError>).urlBase
49
+ }
50
+
51
+ static get fullCommandName (): string {
52
+ const path = []
53
+
54
+ if (this.organizationName != null && this.organizationName !== "GlobalOrganization") {
55
+ path.push(this.organizationName)
56
+ }
57
+ if (this.domainName != null && this.domainName !== "GlobalDomain") {
58
+ path.push(this.domainName)
59
+ }
60
+ if (this.commandName != null) {
61
+ path.push(this.commandName)
62
+ }
63
+ return path.join('::')
64
+ }
65
+
66
+ get fullCommandName (): string {
67
+ return (this.constructor as typeof RemoteCommand<Inputs, Result, CommandError>).fullCommandName
68
+ }
69
+
70
+ async run (): Promise<Outcome<Result, CommandError>> {
71
+ const url = `${this.urlBase}/run/${this.fullCommandName}`
72
+
73
+ const response = await fetch(url, {
74
+ method: 'POST',
75
+ headers: { 'Content-Type': 'application/json' },
76
+ body: JSON.stringify(this.inputs)
77
+ })
78
+
79
+ if (response.ok) {
80
+ return new SuccessfulOutcome<Result, CommandError>(await response.json())
81
+ } else if (response.status === 422) {
82
+ return new ErrorOutcome<Result, CommandError>(await response.json())
83
+ } else {
84
+ throw new Error(`not sure how to handle ${await response.text()}`)
85
+ }
86
+ }
87
+ }