foobara-typescript-remote-command-generator 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+ }