ecoportal-api-graphql 1.3.10 → 1.3.12
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.
- checksums.yaml +4 -4
- data/.ai-assistance/code/diff_pairing_engine.md +243 -0
- data/.ai-assistance/code/filter_contract_matrix.md +177 -0
- data/.ai-assistance/code/graphql_domain_knowledge.md +20 -10
- data/.ai-assistance/code/template_diff_pairing_domain.md +175 -0
- data/.ai-assistance/code/workflow-command-guide.md +28 -0
- data/.ai-assistance/projects/ooze-graphql-native-migration/INVENTORY.md +136 -0
- data/.ai-assistance/projects/ooze-graphql-native-migration/TODO.md +6 -1
- data/.ai-assistance/projects/qa-services-delivery/DECISIONS.md +93 -0
- data/.ai-assistance/projects/qa-services-delivery/INTENT.md +76 -0
- data/.ai-assistance/projects/qa-services-delivery/PHASE3-SCOPE.md +115 -0
- data/.ai-assistance/projects/qa-services-delivery/ROADMAP.md +99 -0
- data/.ai-assistance/projects/qa-services-delivery/TODO.md +81 -0
- data/.ai-assistance/projects/template-automatic-build-maintenance/INTENT.md +87 -0
- data/.ai-assistance/projects/template-automatic-build-maintenance/TODO.md +108 -0
- data/.ai-assistance/projects/template-diff-deploy/INTENT.md +12 -0
- data/.ai-assistance/projects/template-diff-deploy/TODO.md +9 -0
- data/.ai-assistance/projects/template-maintenance/PHASE0-FINDINGS.md +93 -0
- data/.ai-assistance/projects/template-maintenance/README.md +14 -0
- data/.ai-assistance/skills/procedural-memory/SKILL.md +319 -0
- data/.ai-assistance/standards-version.json +21 -20
- data/CHANGELOG.md +119 -0
- data/CLAUDE.md +10 -0
- data/docs/self-docs/ARCHITECTURE.md +88 -0
- data/docs/self-docs/CHANGES.jsonl +7 -0
- data/docs/self-docs/CONVENTIONS.md +74 -0
- data/docs/self-docs/INTEGRATIONS.md +63 -0
- data/docs/self-docs/OVERVIEW.md +51 -0
- data/docs/self-docs/STATUS.md +69 -0
- data/docs/self-docs/self-docs-index.json +39 -0
- data/docs/worklog.md +256 -0
- data/ecoportal-api-graphql.gemspec +1 -1
- data/lib/ecoportal/api/common/graphql/model/diffable.rb +54 -54
- data/lib/ecoportal/api/graphql/base/action.rb +43 -43
- data/lib/ecoportal/api/graphql/base/contractor_entity/member_changes.rb +67 -67
- data/lib/ecoportal/api/graphql/base/page/data_field/collection.rb +7 -0
- data/lib/ecoportal/api/graphql/base/page/data_field/contractor_entities.rb +28 -28
- data/lib/ecoportal/api/graphql/base/page/data_field/file_field.rb +25 -25
- data/lib/ecoportal/api/graphql/base/page/data_field/image_gallery.rb +24 -24
- data/lib/ecoportal/api/graphql/base/page/data_field.rb +1 -1
- data/lib/ecoportal/api/graphql/base/page/section_collection.rb +85 -79
- data/lib/ecoportal/api/graphql/base/preset_view.rb +17 -17
- data/lib/ecoportal/api/graphql/base/register.rb +18 -18
- data/lib/ecoportal/api/graphql/builder/template_builder.rb +174 -0
- data/lib/ecoportal/api/graphql/builder.rb +17 -16
- data/lib/ecoportal/api/graphql/compat/filter_translator.rb +63 -28
- data/lib/ecoportal/api/graphql/concerns/page_compat.rb +51 -51
- data/lib/ecoportal/api/graphql/concerns.rb +14 -14
- data/lib/ecoportal/api/graphql/diff/change.rb +59 -0
- data/lib/ecoportal/api/graphql/diff/command_synthesizer.rb +329 -0
- data/lib/ecoportal/api/graphql/diff/cross_object_diff.rb +165 -0
- data/lib/ecoportal/api/graphql/diff/deploy.rb +121 -0
- data/lib/ecoportal/api/graphql/diff/id_resolver.rb +64 -0
- data/lib/ecoportal/api/graphql/diff/pairing/candidate.rb +32 -0
- data/lib/ecoportal/api/graphql/diff/pairing/engine.rb +173 -0
- data/lib/ecoportal/api/graphql/diff/pairing/ledger.rb +119 -0
- data/lib/ecoportal/api/graphql/diff/pairing/signals.rb +104 -0
- data/lib/ecoportal/api/graphql/diff/strategy.rb +113 -0
- data/lib/ecoportal/api/graphql/diff/version_diff.rb +332 -0
- data/lib/ecoportal/api/graphql/diff.rb +34 -0
- data/lib/ecoportal/api/graphql/file_upload/client.rb +181 -181
- data/lib/ecoportal/api/graphql/fragment/page.rb +85 -85
- data/lib/ecoportal/api/graphql/fragment/pages/common_page_union.rb +1 -0
- data/lib/ecoportal/api/graphql/input/action/update.rb +14 -14
- data/lib/ecoportal/api/graphql/input/contractor_entity/update.rb +41 -41
- data/lib/ecoportal/api/graphql/input/location_structure/apply_commands.rb +47 -47
- data/lib/ecoportal/api/graphql/input/location_structure/draft/add_commands.rb +49 -49
- data/lib/ecoportal/api/graphql/input/location_structure/update_command.rb +27 -27
- data/lib/ecoportal/api/graphql/input/search_conf.rb +436 -367
- data/lib/ecoportal/api/graphql/input/workflow_command/add_field.rb +27 -18
- data/lib/ecoportal/api/graphql/interface/location_structure/command.rb +30 -30
- data/lib/ecoportal/api/graphql/logic/input.rb +26 -26
- data/lib/ecoportal/api/graphql/mutation/action/archive.rb +1 -1
- data/lib/ecoportal/api/graphql/mutation/action/create.rb +1 -1
- data/lib/ecoportal/api/graphql/mutation/action/update.rb +1 -1
- data/lib/ecoportal/api/graphql/mutation/contractor_entity/create.rb +1 -1
- data/lib/ecoportal/api/graphql/mutation/contractor_entity/destroy.rb +1 -1
- data/lib/ecoportal/api/graphql/mutation/contractor_entity/update.rb +1 -1
- data/lib/ecoportal/api/graphql/mutation/kickstand/fail_workflow.rb +1 -1
- data/lib/ecoportal/api/graphql/mutation/kickstand/start_workflow.rb +1 -1
- data/lib/ecoportal/api/graphql/mutation/kickstand/stop_workflow.rb +1 -1
- data/lib/ecoportal/api/graphql.rb +1 -0
- data/lib/ecoportal/api/graphql_version.rb +1 -1
- data/tests/dump_template_model.rb +90 -0
- data/tests/validate_queries.rb +31 -9
- metadata +40 -3
|
@@ -1,79 +1,85 @@
|
|
|
1
|
-
module Ecoportal
|
|
2
|
-
module API
|
|
3
|
-
class GraphQL
|
|
4
|
-
module Base
|
|
5
|
-
module Page
|
|
6
|
-
# Wraps an array of raw section docs as typed Section objects.
|
|
7
|
-
# Provides v2-compatible access patterns:
|
|
8
|
-
# sections.get_by_type(:content_section)
|
|
9
|
-
# sections.doc — raw doc array
|
|
10
|
-
# sections.ordered — sections in response order
|
|
11
|
-
# sections.each { |s| s.components ... }
|
|
12
|
-
class SectionCollection
|
|
13
|
-
include Enumerable
|
|
14
|
-
|
|
15
|
-
def initialize(sections_array)
|
|
16
|
-
@sections = Array(sections_array).map { |s| Section.new(s) }
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def self.from_doc(raw_array)
|
|
20
|
-
new(Array(raw_array))
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# Filter by section type. Accepts symbol (:content_section / :split_section)
|
|
24
|
-
# or string ('ContentSection' / 'SplitSection'). Case-insensitive, underscore-tolerant.
|
|
25
|
-
def get_by_type(type_key)
|
|
26
|
-
target = normalise_type(type_key)
|
|
27
|
-
@sections.select { |s| normalise_type(s.type) == target }
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Find section by heading (case-insensitive).
|
|
31
|
-
def get_by_heading(heading)
|
|
32
|
-
@sections.find { |s| s.heading.to_s.downcase == heading.to_s.downcase }
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
#
|
|
36
|
-
def
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def
|
|
72
|
-
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
end
|
|
1
|
+
module Ecoportal
|
|
2
|
+
module API
|
|
3
|
+
class GraphQL
|
|
4
|
+
module Base
|
|
5
|
+
module Page
|
|
6
|
+
# Wraps an array of raw section docs as typed Section objects.
|
|
7
|
+
# Provides v2-compatible access patterns:
|
|
8
|
+
# sections.get_by_type(:content_section)
|
|
9
|
+
# sections.doc — raw doc array
|
|
10
|
+
# sections.ordered — sections in response order
|
|
11
|
+
# sections.each { |s| s.components ... }
|
|
12
|
+
class SectionCollection
|
|
13
|
+
include Enumerable
|
|
14
|
+
|
|
15
|
+
def initialize(sections_array)
|
|
16
|
+
@sections = Array(sections_array).map { |s| Section.new(s) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.from_doc(raw_array)
|
|
20
|
+
new(Array(raw_array))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Filter by section type. Accepts symbol (:content_section / :split_section)
|
|
24
|
+
# or string ('ContentSection' / 'SplitSection'). Case-insensitive, underscore-tolerant.
|
|
25
|
+
def get_by_type(type_key)
|
|
26
|
+
target = normalise_type(type_key)
|
|
27
|
+
@sections.select { |s| normalise_type(s.type) == target }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Find section by heading (case-insensitive).
|
|
31
|
+
def get_by_heading(heading)
|
|
32
|
+
@sections.find { |s| s.heading.to_s.downcase == heading.to_s.downcase }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Find a section by its id (v2 compat: sections.get_by_id). Returns the section or nil.
|
|
36
|
+
def get_by_id(id)
|
|
37
|
+
wanted = id.to_s
|
|
38
|
+
@sections.find { |s| s.id.to_s == wanted }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Raw doc array — v2 compat (scripts iterate sections.doc).
|
|
42
|
+
def doc
|
|
43
|
+
@sections.map(&:doc)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Sections in response order (already ordered from server).
|
|
47
|
+
def ordered
|
|
48
|
+
@sections
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# All data fields across every section in this collection (memoized).
|
|
52
|
+
def components
|
|
53
|
+
@components ||= begin
|
|
54
|
+
all_docs = @sections.flat_map do |s|
|
|
55
|
+
(s.doc['dataFields'] || []) +
|
|
56
|
+
(s.doc['leftDataFields'] || []) +
|
|
57
|
+
(s.doc['rightDataFields'] || [])
|
|
58
|
+
end
|
|
59
|
+
DataField::Collection.from_doc(all_docs)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def each(&block)
|
|
64
|
+
@sections.each(&block)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def empty?
|
|
68
|
+
@sections.empty?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def length
|
|
72
|
+
@sections.length
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def normalise_type(type_key)
|
|
78
|
+
type_key.to_s.downcase.delete('_') if type_key
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
module Ecoportal
|
|
2
|
-
module API
|
|
3
|
-
class GraphQL
|
|
4
|
-
module Base
|
|
5
|
-
# A register preset view — saved column/sort/filter configuration for previewPages.
|
|
6
|
-
# Scripts use the id as `presetViewId` in register.previewPages queries.
|
|
7
|
-
class PresetView < Logic::BaseModel
|
|
8
|
-
passkey :id
|
|
9
|
-
passthrough :name, :registerId
|
|
10
|
-
passboolean :global
|
|
11
|
-
passarray :fieldConfigurations
|
|
12
|
-
passthrough :sorter # { key:, direction:, missing: }
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
1
|
+
module Ecoportal
|
|
2
|
+
module API
|
|
3
|
+
class GraphQL
|
|
4
|
+
module Base
|
|
5
|
+
# A register preset view — saved column/sort/filter configuration for previewPages.
|
|
6
|
+
# Scripts use the id as `presetViewId` in register.previewPages queries.
|
|
7
|
+
class PresetView < Logic::BaseModel
|
|
8
|
+
passkey :id
|
|
9
|
+
passthrough :name, :registerId
|
|
10
|
+
passboolean :global
|
|
11
|
+
passarray :fieldConfigurations
|
|
12
|
+
passthrough :sorter # { key:, direction:, missing: }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
module Ecoportal
|
|
2
|
-
module API
|
|
3
|
-
class GraphQL
|
|
4
|
-
module Base
|
|
5
|
-
class Register < Logic::BaseModel
|
|
6
|
-
passkey :id
|
|
7
|
-
passthrough :name, :iconName, :moduleType, :registerStatus
|
|
8
|
-
passthrough :descriptionText, :descriptionImageUrl
|
|
9
|
-
passboolean :shown, :creatable, :attachable, :permissioned
|
|
10
|
-
passboolean :contentShareable, :enableEmailOut, :isMobile
|
|
11
|
-
passboolean :restrictTemplatesAmount
|
|
12
|
-
passthrough :restrictedAmountOfTemplates
|
|
13
|
-
passarray :filterTags
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
1
|
+
module Ecoportal
|
|
2
|
+
module API
|
|
3
|
+
class GraphQL
|
|
4
|
+
module Base
|
|
5
|
+
class Register < Logic::BaseModel
|
|
6
|
+
passkey :id
|
|
7
|
+
passthrough :name, :iconName, :moduleType, :registerStatus
|
|
8
|
+
passthrough :descriptionText, :descriptionImageUrl
|
|
9
|
+
passboolean :shown, :creatable, :attachable, :permissioned
|
|
10
|
+
passboolean :contentShareable, :enableEmailOut, :isMobile
|
|
11
|
+
passboolean :restrictTemplatesAmount
|
|
12
|
+
passthrough :restrictedAmountOfTemplates
|
|
13
|
+
passarray :filterTags
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
module Ecoportal
|
|
2
|
+
module API
|
|
3
|
+
class GraphQL
|
|
4
|
+
module Builder
|
|
5
|
+
# Build-from-scratch emitter for a template/workflow: turns a DECLARATIVE spec
|
|
6
|
+
# (stages -> sections -> fields -> options / config / gauge-stops) into an ordered batch of
|
|
7
|
+
# built `WorkflowCommand` hashes, ready for `Builder::Template#create(commands:)` /
|
|
8
|
+
# `#update(model, commands:)` (which wrap `executeWorkflowCommands`).
|
|
9
|
+
#
|
|
10
|
+
# This is the gem-level, reusable BUILD emitter (previously a one-off in eco-helpers samples).
|
|
11
|
+
# It shares ONE emission layer with the DIFF-DEPLOY pipeline: both produce an ordered
|
|
12
|
+
# `WorkflowCommand` batch, and both use the SAME `placeholderId` id-threading primitive that
|
|
13
|
+
# `Diff::CommandSynthesizer` uses — a newly-created node is addressed by a client-chosen
|
|
14
|
+
# `placeholderId`, and every later command in the same batch that references it substitutes
|
|
15
|
+
# that placeholder (the server has not assigned real ids yet). Here every node is brand new,
|
|
16
|
+
# so the whole batch is threaded through placeholders.
|
|
17
|
+
#
|
|
18
|
+
# SPEC SHAPE (all keys optional unless noted; only VALID command inputs are emitted):
|
|
19
|
+
#
|
|
20
|
+
# {
|
|
21
|
+
# stages: [
|
|
22
|
+
# { name: 'Report', ordering: 0, sections: [ # addStage(name, ordering)
|
|
23
|
+
# { layout: 'ONE_COLUMN', fields: [ # addSection(layout) + addStageSection
|
|
24
|
+
# { field_type: 'PlainText', label: 'Description', column: 0,
|
|
25
|
+
# description: 'ID:token' }, # addField (+ editFieldConfiguration for description)
|
|
26
|
+
# { field_type: 'Select', label: 'Severity',
|
|
27
|
+
# options: [ { label: 'Low', weight: 0 }, ... ], # addSelectFieldOption
|
|
28
|
+
# config: { select: { multiple: false } } }, # editFieldConfiguration(byType:)
|
|
29
|
+
# { field_type: 'Gauge', label: 'Risk',
|
|
30
|
+
# stops: [ { threshold: 0, color: '#0f0' }, ... ], # addGaugeFieldStop
|
|
31
|
+
# config: { gauge: { max: 100.0 } } }
|
|
32
|
+
# ] }
|
|
33
|
+
# ] }
|
|
34
|
+
# ]
|
|
35
|
+
# }
|
|
36
|
+
#
|
|
37
|
+
# ORDER — addStage -> addSection -> addStageSection -> addField -> (addSelectFieldOption |
|
|
38
|
+
# addGaugeFieldStop | editFieldConfiguration). This is the dependency-safe order the workflow
|
|
39
|
+
# command bus expects; placeholderId resolution makes the intra-batch references valid.
|
|
40
|
+
#
|
|
41
|
+
# SAFETY — only keys present in each command input's VALID_KEYS are emitted (the input classes
|
|
42
|
+
# slice + compact). A spec key with no corresponding command input is dropped, not fabricated.
|
|
43
|
+
# `field_type` shape and per-type config (`byType`) are NOT invented: config is passed through
|
|
44
|
+
# under its byType sub-hash exactly as given (the caller supplies confirmed byType keys), and
|
|
45
|
+
# is emitted only for a field that carries one.
|
|
46
|
+
class TemplateBuilder
|
|
47
|
+
# @param spec [Hash] the declarative template spec (see class doc). String or symbol keys.
|
|
48
|
+
def initialize(spec)
|
|
49
|
+
@spec = symbolize(spec || {})
|
|
50
|
+
@commands = []
|
|
51
|
+
@counter = 0
|
|
52
|
+
@built = false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Ordered Array of built command hashes ({ commandKey => input }), placeholder-threaded.
|
|
56
|
+
def commands
|
|
57
|
+
return @commands if @built
|
|
58
|
+
|
|
59
|
+
@built = true
|
|
60
|
+
build_all
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Ready to hand to `Builder::Template#create(commands: builder.to_commands)`.
|
|
64
|
+
alias_method :to_commands, :commands
|
|
65
|
+
|
|
66
|
+
def to_h
|
|
67
|
+
{ commands: commands }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def build_all
|
|
73
|
+
Array(@spec[:stages]).each_with_index { |stage, idx| emit_stage(stage, idx) }
|
|
74
|
+
@commands
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def emit_stage(stage, index)
|
|
78
|
+
stage_ph = placeholder(:stg)
|
|
79
|
+
add(:addStage, name: stage[:name], ordering: stage.fetch(:ordering, index), placeholderId: stage_ph)
|
|
80
|
+
Array(stage[:sections]).each { |section| emit_section(section, stage_ph) }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def emit_section(section, stage_ph)
|
|
84
|
+
section_ph = placeholder(:sec)
|
|
85
|
+
add(:addSection, layout: section[:layout], placeholderId: section_ph)
|
|
86
|
+
# Attach the freshly-created section to its (freshly-created) stage.
|
|
87
|
+
add(:addStageSection, stageId: stage_ph, sectionId: section_ph)
|
|
88
|
+
emit_section_heading(section, section_ph)
|
|
89
|
+
Array(section[:fields]).each { |field| emit_field(field, stage_ph, section_ph) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# A section's heading is not an addSection input; set it via editSectionHeader after create.
|
|
93
|
+
def emit_section_heading(section, section_ph)
|
|
94
|
+
heading = section[:heading]
|
|
95
|
+
return if heading.nil?
|
|
96
|
+
|
|
97
|
+
add(:editSectionHeader, sectionId: section_ph, header: heading)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def emit_field(field, stage_ph, section_ph)
|
|
101
|
+
field_ph = placeholder(:fld)
|
|
102
|
+
add(:addField,
|
|
103
|
+
fieldType: field[:field_type] || field[:fieldType],
|
|
104
|
+
label: field[:label],
|
|
105
|
+
stageId: stage_ph,
|
|
106
|
+
sectionId: section_ph,
|
|
107
|
+
column: field[:column],
|
|
108
|
+
placeholderId: field_ph)
|
|
109
|
+
emit_description(field, field_ph)
|
|
110
|
+
emit_options(field, field_ph)
|
|
111
|
+
emit_stops(field, field_ph)
|
|
112
|
+
emit_config(field, field_ph)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# A field's `description` is NOT an addField input (the schema's WorkflowAddFieldInput has
|
|
116
|
+
# no `description` key — VERIFIED against the 20260605 dump). It is a top-level key of
|
|
117
|
+
# editFieldConfiguration, so we persist it with a follow-up editFieldConfiguration
|
|
118
|
+
# addressed to the freshly-created field placeholder. Emitted only when supplied. This is
|
|
119
|
+
# the seam the CSV/hidden-field identity token rides on. NOTE: `required` has no schema
|
|
120
|
+
# input key at all, so it is intentionally NOT emitted (a spec `required:` is ignored).
|
|
121
|
+
def emit_description(field, field_ph)
|
|
122
|
+
description = field[:description]
|
|
123
|
+
return if description.nil?
|
|
124
|
+
|
|
125
|
+
add(:editFieldConfiguration, data_field_id: field_ph, description: description)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def emit_options(field, field_ph)
|
|
129
|
+
Array(field[:options]).each do |opt|
|
|
130
|
+
add(:addSelectFieldOption, data_field_id: field_ph, label: opt[:label], weight: opt[:weight])
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def emit_stops(field, field_ph)
|
|
135
|
+
Array(field[:stops]).each do |stop|
|
|
136
|
+
add(:addGaugeFieldStop, data_field_id: field_ph, threshold: stop[:threshold], color: stop[:color])
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Per-type field configuration passed through under its byType sub-hash exactly as supplied
|
|
141
|
+
# (e.g. { select: { multiple: true } }, { gauge: { max: 100.0 } }). Emitted only when
|
|
142
|
+
# present; keys are validated/sliced by the EditFieldConfiguration input, never fabricated.
|
|
143
|
+
def emit_config(field, field_ph)
|
|
144
|
+
config = field[:config]
|
|
145
|
+
return if config.nil? || config.empty?
|
|
146
|
+
|
|
147
|
+
add(:editFieldConfiguration, data_field_id: field_ph, byType: symbolize(config))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Build one command and append it. Reuses the SAME builder the diff synthesizer uses, so
|
|
151
|
+
# BUILD and DIFF-DEPLOY emit through one layer.
|
|
152
|
+
def add(command_key, **kwargs)
|
|
153
|
+
@commands << Ecoportal::API::GraphQL::Input::WorkflowCommand.build(command_key, **kwargs)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# A deterministic client-chosen placeholderId — the id-threading primitive shared with the
|
|
157
|
+
# diff synthesizer. Prefix marks the node kind for readability of the emitted batch.
|
|
158
|
+
def placeholder(prefix)
|
|
159
|
+
@counter += 1
|
|
160
|
+
"ph_#{prefix}_#{@counter}"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def symbolize(value)
|
|
164
|
+
case value
|
|
165
|
+
when Hash then value.each_with_object({}) { |(k, v), h| h[k.to_sym] = symbolize(v) }
|
|
166
|
+
when Array then value.map { |e| symbolize(e) }
|
|
167
|
+
else value
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
module Ecoportal
|
|
2
|
-
module API
|
|
3
|
-
class GraphQL
|
|
4
|
-
module Builder
|
|
5
|
-
end
|
|
6
|
-
end
|
|
7
|
-
end
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
require_relative 'builder/location_structure'
|
|
11
|
-
require_relative 'builder/action'
|
|
12
|
-
require_relative 'builder/contractor_entity'
|
|
13
|
-
require_relative 'builder/register'
|
|
14
|
-
require_relative 'builder/page'
|
|
15
|
-
require_relative 'builder/kickstand'
|
|
16
|
-
require_relative 'builder/template'
|
|
1
|
+
module Ecoportal
|
|
2
|
+
module API
|
|
3
|
+
class GraphQL
|
|
4
|
+
module Builder
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
require_relative 'builder/location_structure'
|
|
11
|
+
require_relative 'builder/action'
|
|
12
|
+
require_relative 'builder/contractor_entity'
|
|
13
|
+
require_relative 'builder/register'
|
|
14
|
+
require_relative 'builder/page'
|
|
15
|
+
require_relative 'builder/kickstand'
|
|
16
|
+
require_relative 'builder/template'
|
|
17
|
+
require_relative 'builder/template_builder'
|
|
@@ -9,9 +9,12 @@ module Ecoportal
|
|
|
9
9
|
# callers may also pass already-translated hashes or SearchConf instances directly.
|
|
10
10
|
#
|
|
11
11
|
# v2 → GraphQL translation table:
|
|
12
|
-
# date_filter lbound/ubound → gte/lte (
|
|
12
|
+
# date_filter lbound/ubound → gte/lte; Date FIELD (key 'date.zXXXX') →
|
|
13
|
+
# range_date_filter + membranes.date path; system date → date_filter
|
|
13
14
|
# text_filter {query, key} → match_filter {key, value} (partial text)
|
|
14
|
-
# options_filter
|
|
15
|
+
# options_filter Select FIELD (key 'select_str.zXXXX') → has_any_filter
|
|
16
|
+
# {key, path, field_key: 'selected', values} (doesnt_have_any_filter to
|
|
17
|
+
# exclude); system option key → one_of_filter/none_of_filter {key, values}
|
|
15
18
|
# tag_filter passed through (same ES backend, same format)
|
|
16
19
|
# register_filter passed through
|
|
17
20
|
# and_filter / or_filter passed through
|
|
@@ -43,12 +46,18 @@ module Ecoportal
|
|
|
43
46
|
# exact one: it matches the field's `.exact` keyword subfield (backend
|
|
44
47
|
# NewEp::Es::Filters::Pages::IsFilter → ExactFilter on `membranes.<type>.exact`),
|
|
45
48
|
# whereas match_filter/contains_filter hit the analyzed `.value` and miss exact SKUs.
|
|
46
|
-
#
|
|
49
|
+
# A register FIELD key ('plain_text.zXXXX') needs the nested membranes.<type> path;
|
|
50
|
+
# a SYSTEM key (external_id/id/state/creator_id/source) must NOT carry a path — the
|
|
51
|
+
# backend routes it to Global::ExactFilter which joins [@path,@key] into the term key,
|
|
52
|
+
# so a spurious 'membranes.external_id' yields term 'membranes.external_id.external_id'
|
|
53
|
+
# → 0 results (proof: pages/is_filter.rb:36-55 passes path: @path to Global::ExactFilter;
|
|
54
|
+
# global/exact_filter.rb:18 builds the term as [@path, @key].compact.join(".")).
|
|
47
55
|
# CAVEAT: case-insensitive and only the first 64 chars are significant (values are
|
|
48
56
|
# lowercased + truncated at index time). No case-sensitive/>64-char exact exists.
|
|
49
57
|
def translate_exact_filter(filter)
|
|
50
58
|
key = filter['key'] || filter[:key]
|
|
51
|
-
path = filter['path'] || filter[:path]
|
|
59
|
+
path = filter['path'] || filter[:path]
|
|
60
|
+
path ||= membrane_path_for(key) if membrane_field?(key)
|
|
52
61
|
value = filter['value'] || filter[:value] || filter['query'] || filter[:query]
|
|
53
62
|
{ operation: 'is_filter', params: { key: key, path: path, value: value }.compact }
|
|
54
63
|
end
|
|
@@ -59,6 +68,14 @@ module Ecoportal
|
|
|
59
68
|
prefix.empty? ? nil : "membranes.#{prefix}"
|
|
60
69
|
end
|
|
61
70
|
|
|
71
|
+
# A register FIELD key carries a `<type>.<hash>` shape (e.g. 'date.zab1bddc3',
|
|
72
|
+
# 'select_str.z223beb6d'); system/top-level keys (created_at, id, external_id) have
|
|
73
|
+
# no field-type prefix. Field filters need the nested membranes.<type> path (and,
|
|
74
|
+
# for options, a field_key), which system filters do not.
|
|
75
|
+
def membrane_field?(key)
|
|
76
|
+
key.to_s.include?('.')
|
|
77
|
+
end
|
|
78
|
+
|
|
62
79
|
# v2 text_filter {query, key} → GraphQL match_filter (partial/full-text on key).
|
|
63
80
|
def translate_text_filter(filter)
|
|
64
81
|
{
|
|
@@ -70,34 +87,52 @@ module Ecoportal
|
|
|
70
87
|
}
|
|
71
88
|
end
|
|
72
89
|
|
|
73
|
-
# v2 options_filter {chosen, key, mode} →
|
|
74
|
-
#
|
|
90
|
+
# v2 options_filter {chosen, key, mode} → GraphQL.
|
|
91
|
+
# A register Select FIELD (key like 'select_str.zXXXX') filters via
|
|
92
|
+
# has_any_filter / doesnt_have_any_filter over its nested path
|
|
93
|
+
# (membranes.select_str) with field_key 'selected' and the chosen option ids as
|
|
94
|
+
# `values` (backend NewEp::Es::Filters::Pages::*::HasAnyFilter). A system/top-level
|
|
95
|
+
# option key (e.g. 'id') keeps one_of_filter / none_of_filter with {key, values}.
|
|
75
96
|
def translate_options_filter(filter)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
97
|
+
key = filter['key'] || filter[:key]
|
|
98
|
+
values = Array(filter['chosen'] || filter[:chosen])
|
|
99
|
+
exclude = exclude_mode?(filter)
|
|
100
|
+
|
|
101
|
+
unless membrane_field?(key)
|
|
102
|
+
op = exclude ? 'none_of_filter' : 'one_of_filter'
|
|
103
|
+
return { operation: op, params: { key: key, values: values } }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
op = exclude ? 'doesnt_have_any_filter' : 'has_any_filter'
|
|
107
|
+
path = filter['path'] || filter[:path] || membrane_path_for(key)
|
|
108
|
+
field_key = filter['field_key'] || filter[:field_key] || 'selected'
|
|
109
|
+
{ operation: op, params: { key: key, path: path, field_key: field_key, values: values } }
|
|
85
110
|
end
|
|
86
111
|
|
|
112
|
+
# v2 date_filter → GraphQL. A register Date FIELD (key like 'date.zXXXX') filters
|
|
113
|
+
# via range_date_filter on its nested path (membranes.date); a system/top-level date
|
|
114
|
+
# (created_at/updated_at) uses date_filter. Both share range params (gte/lte/time_zone)
|
|
115
|
+
# — range_date_filter additionally carries `path` (backend BaseRangeDateFilter < DateFilter).
|
|
87
116
|
def translate_date_filter(filter)
|
|
88
|
-
key
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
117
|
+
key = filter['key'] || filter[:key]
|
|
118
|
+
field = membrane_field?(key)
|
|
119
|
+
path = filter['path'] || filter[:path]
|
|
120
|
+
path ||= membrane_path_for(key) if field
|
|
121
|
+
|
|
122
|
+
params = { key: key, time_zone: filter['time_zone'] || filter[:time_zone] || 'UTC' }
|
|
123
|
+
params[:path] = path if path
|
|
124
|
+
lbound = filter['lbound'] || filter[:lbound]
|
|
125
|
+
ubound = filter['ubound'] || filter[:ubound]
|
|
126
|
+
params[:gte] = lbound if lbound
|
|
127
|
+
params[:lte] = ubound if ubound
|
|
128
|
+
|
|
129
|
+
{ operation: field ? 'range_date_filter' : 'date_filter', params: params }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# "exclude" style modes map to the negative filter operation.
|
|
133
|
+
def exclude_mode?(filter)
|
|
134
|
+
mode = (filter['mode'] || filter[:mode]).to_s.downcase
|
|
135
|
+
%w[none exclude not not_in].include?(mode)
|
|
101
136
|
end
|
|
102
137
|
end
|
|
103
138
|
end
|