Module: AxHub::Data
- Defined in:
- lib/axhub_sdk/data.rb,
lib/axhub_sdk/data/client.rb,
lib/axhub_sdk/data/client.rb,
lib/axhub_sdk/data/errors.rb,
lib/axhub_sdk/data/dsl/ops.rb,
lib/axhub_sdk/data/discover.rb,
lib/axhub_sdk/data/dsl/schema.rb,
lib/axhub_sdk/data/pagination.rb,
lib/axhub_sdk/data/projection.rb,
lib/axhub_sdk/data/schema_cache.rb,
lib/axhub_sdk/data/dsl/validation.rb,
lib/axhub_sdk/data/where_serializer.rb
Defined Under Namespace
Classes: AppDataFactory, AppScope, DataClient, DataColumn, DataTableClient, DataTableSchema, IntrospectFailedError, InvalidCursorError, LegacyCursorError, LikeBuilder, ListAllItem, PaginatedList, ScanLimitExceededError, SchemaCache, TableNotFoundError, TenantDataFactory, TenantScope, ValidationError, WhereBuilder
Constant Summary
collapse
- MAX_LIKE_PATTERN_LENGTH =
Predicate DSL: where(col).eq(v), and_(…), or_/not_/raw plus LIKE escaping and ReDoS guards (mirrors node/python dsl/ops).
Query expressions are plain symbol-keyed Hashes:
{ op: :eq|:ne|:gt|:gte|:lt|:lte|:like, column: "c", value: v }
{ op: :in, column: "c", values: [...] }
{ op: :and|:or, clauses: [...] }
{ op: :not, clause: expr }
{ op: :raw, sql: "...", params?: [...] }
Only and(eq/ne/gt/gte/lt/lte/in/like) and bare atoms are pushable to the live backend; or/not/raw raise in the where-serializer (mirrors node).
1024
- MAX_CONSECUTIVE_WILDCARDS =
4
- MAX_LIKE_ALTERNATION_SEGMENTS =
6
- ESCAPE_LIKE_RE =
/[\\%_]/
- APP_LOOKUP_PAGE_SIZE =
Runtime schema introspection, with appId-resolution PRIMARY and the slug /inspect endpoint as a best-effort fallback, plus error normalization (mirrors node/python discover).
Primary: GET /api/v1/apps?tenant_slug=… -> GET /api/v1/apps/appId/tables/table Fallback: GET /api/v1/tenants/t/apps/a/tables/table/inspect Neither endpoint has a generated operation-id, so discover goes through the raw-path transport. camelize: true here so table_name/tableName both resolve (inspect payload is metadata, not user row data).
100
- APP_LOOKUP_MAX_PAGES =
10
- APP_LOOKUP_BUDGET_MS =
5_000
- FORBIDDEN_COLUMN_NAMES =
%w[__proto__ constructor prototype].freeze
- COLUMN_NAME_RE =
/\A[A-Za-z_][A-Za-z0-9_]*\z/
- MAX_CURSOR_TOKEN_LENGTH =
Offset pagination helpers (subset of node core pagination that the data ergonomic layer depends on).
Ported: serialize_order_by / normalize_order_by, is_v2_cursor, MAX_CURSOR_TOKEN_LENGTH, list_all, and the PaginatedList / ListAllItem result shapes. Keyset encode/decode is intentionally NOT ported: the live AX Hub data API is offset-only, so the data layer only needs the order-by normalizer and the cursor-shape guards used to reject legacy keyset tokens.
4096
- DEFAULT_SCHEMA_CACHE_TTL_MS =
Per-client schema cache for runtime data.discover (mirrors node/python schema-cache).
Uses an insertion-ordered Ruby Hash for deterministic LRU eviction (delete+reinsert = move-to-end on read/write, ‘shift` = evict oldest) and a negative-TTL stale-while-error window: a transient 5xx during refresh keeps the previous entry alive briefly instead of evicting it. The node version de-dupes concurrent in-flight loads; the sync Ruby port omits the in-flight map (no concurrency within a single synchronous call).
5 * 60_000
- DEFAULT_SCHEMA_CACHE_MAX_ENTRIES =
1000
- DEFAULT_SCHEMA_CACHE_NEGATIVE_TTL_MS =
30_000
- PUSHABLE_BINARY =
Serialize the predicate DSL into backend filter query params (mirrors node where-serializer + python where_serializer).
Each pushable atom becomes column=<op>.<value> (PostgREST-style). Repeated columns collapse into an array so the transport emits repeated query params (URI.encode_www_form repeats array-valued keys). Only top-level and(…) of pushable atoms and bare atoms are accepted; or/not/raw and nested-and raise ValidationError — this matches the live backend’s filter grammar.
%i[eq ne gt gte lt lte like].freeze
Class Method Summary
collapse
-
._append_query(out, key, value) ⇒ Object
-
._clamp_per_page(value) ⇒ Object
-
._collect_pushable_filters(expr, allow_and:) ⇒ Object
-
._column_type_to_def(col_type) ⇒ Object
-
._encode(value) ⇒ Object
-
._fetch_app_id_inspect(client, tenant_slug, app_slug, table) ⇒ Object
-
._fetch_slug_inspect(client, tenant_slug, app_slug, table) ⇒ Object
-
._normalize_discover_error(err, table) ⇒ Object
-
._not_found?(err) ⇒ Boolean
-
._read(obj, key) ⇒ Object
Read an attribute from either a duck-typed object or a Hash.
-
._reject_legacy_page_options(after, before, direction, _table_name) ⇒ Object
-
._resolve_app_id(client, tenant_slug, app_slug) ⇒ Object
-
._resolve_offset_page(cursor, page, table_name) ⇒ Object
-
._safe_parse(validator, data) ⇒ Object
-
._stringify(value) ⇒ Object
-
._stringify_keys(hash) ⇒ Object
-
._validate_plain_cursor(cursor, _table_name) ⇒ Object
-
.and_(*clauses) ⇒ Object
-
.assert_safe_like_pattern(pattern) ⇒ Object
Reject LIKE patterns that translate to catastrophic-backtracking regex shapes (mirrors node assertSafeLikePattern).
-
.define_schema(table_or_input, columns = nil, validate: nil) ⇒ Object
Define a data table schema.
-
.escape_like(value) ⇒ Object
-
.fetch_discovered_schema(client, tenant_slug, app_slug, table, fresh: nil, ttl_ms: nil) ⇒ Object
-
.is_v2_cursor(token) ⇒ Object
-
.list_all(fetcher, opts = {}) ⇒ Object
Drive a paginated fetcher to exhaustion, yielding each item and a drift marker when the backend total grows mid-iteration (mirrors node listAll).
-
.normalize_order_by(order_by) ⇒ Object
order_by = String | Array<{ field: String, dir?: “asc”|“desc” }>.
-
.not_(clause) ⇒ Object
-
.or_(*clauses) ⇒ Object
-
.project_row(row, select) ⇒ Object
-
.project_rows(rows, select) ⇒ Object
-
.raw(sql, params = nil) ⇒ Object
-
.run_schema_validation(schema, data, mode) ⇒ Object
Validate ‘data` against schema.validate before any network request.
-
.schema_cache_key(tenant_slug, app_slug, table) ⇒ Object
-
.schema_from_inspect_result(table, raw) ⇒ Object
-
.serialize_order_by(order_by) ⇒ Object
-
.serialize_select(select) ⇒ Object
-
.serialize_where(expr) ⇒ Object
-
.validate_select_columns(schema, select) ⇒ Object
-
.validator_like?(value) ⇒ Boolean
-
.where(column) ⇒ Object
Start a predicate for a column.
Class Method Details
._append_query(out, key, value) ⇒ Object
31
32
33
34
35
36
37
38
39
|
# File 'lib/axhub_sdk/data/where_serializer.rb', line 31
def _append_query(out, key, value)
if !out.key?(key)
out[key] = value
elsif out[key].is_a?(Array)
out[key] << value
else
out[key] = [out[key], value]
end
end
|
._clamp_per_page(value) ⇒ Object
30
31
32
33
34
35
|
# File 'lib/axhub_sdk/data/client.rb', line 30
def _clamp_per_page(value)
return nil if value.nil?
return 100 unless value.is_a?(Numeric) && value.finite?
[100, [1, value.to_i].max].min
end
|
._collect_pushable_filters(expr, allow_and:) ⇒ Object
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
|
# File 'lib/axhub_sdk/data/where_serializer.rb', line 41
def _collect_pushable_filters(expr, allow_and:)
op = expr[:op]
if PUSHABLE_BINARY.include?(op)
return [{ column: expr[:column], value: "#{op}.#{_stringify(expr[:value])}" }]
end
if op == :in
values = expr[:values].map { |v| _stringify(v) }
bad = values.find { |v| v.include?(',') }
unless bad.nil?
raise ValidationError.new(
"IN filter values cannot contain commas because the live backend uses comma-separated IN lists (bad value: #{bad})",
'filter_in_comma'
)
end
return [{ column: expr[:column], value: "in.#{values.join(',')}" }]
end
if op == :and && allow_and
out = []
expr[:clauses].each { |clause| out.concat(_collect_pushable_filters(clause, allow_and: false)) }
return out
end
raise ValidationError.new(
"Data where clause '#{op}' cannot be pushed to the live backend; use top-level and(eq/ne/gt/gte/lt/lte/in/like) only",
'unsupported_filter'
)
end
|
._column_type_to_def(col_type) ⇒ Object
130
131
132
133
134
135
136
137
138
139
140
|
# File 'lib/axhub_sdk/data/discover.rb', line 130
def _column_type_to_def(col_type)
case col_type
when 'uuid' then 'uuid'
when 'int', 'integer', 'bigint' then 'integer'
when 'float', 'numeric', 'double precision', 'real' then 'number'
when 'bool', 'boolean' then 'boolean'
when 'timestamp', 'timestamptz', 'timestamp with time zone' then 'timestamp'
when 'json', 'jsonb' then 'json'
else 'string' end
end
|
._encode(value) ⇒ Object
26
27
28
|
# File 'lib/axhub_sdk/data/client.rb', line 26
def _encode(value)
URI.encode_www_form_component(value.to_s)
end
|
._fetch_app_id_inspect(client, tenant_slug, app_slug, table) ⇒ Object
55
56
57
58
59
60
61
62
|
# File 'lib/axhub_sdk/data/discover.rb', line 55
def _fetch_app_id_inspect(client, tenant_slug, app_slug, table)
app_id = _resolve_app_id(client, tenant_slug, app_slug)
raise TableNotFoundError.new("Dynamic data table '#{table}' was not found") if app_id.nil? || app_id.empty?
path = "/api/v1/apps/#{_encode(app_id)}/tables/#{_encode(table)}"
raw = client.request_raw('GET', path, camelize: true)
schema_from_inspect_result(table, raw)
end
|
._fetch_slug_inspect(client, tenant_slug, app_slug, table) ⇒ Object
49
50
51
52
53
|
# File 'lib/axhub_sdk/data/discover.rb', line 49
def _fetch_slug_inspect(client, tenant_slug, app_slug, table)
path = "/api/v1/tenants/#{_encode(tenant_slug)}/apps/#{_encode(app_slug)}/tables/#{_encode(table)}/inspect"
raw = client.request_raw('GET', path, camelize: true)
schema_from_inspect_result(table, raw)
end
|
._normalize_discover_error(err, table) ⇒ Object
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
|
# File 'lib/axhub_sdk/data/discover.rb', line 94
def _normalize_discover_error(err, table)
return err if err.is_a?(TableNotFoundError) || err.is_a?(IntrospectFailedError) || err.is_a?(ScanLimitExceededError)
if _not_found?(err)
return TableNotFoundError.new(
"Dynamic data table '#{table}' was not found",
request_id: (err.respond_to?(:request_id) ? err.request_id : nil)
)
end
status = err.respond_to?(:status) ? err.status : nil
if status.is_a?(Integer) && status >= 500
return IntrospectFailedError.new(
"Failed to introspect dynamic data table '#{table}'",
status: status,
retryable: (err.respond_to?(:retryable) ? !!err.retryable : false),
request_id: (err.respond_to?(:request_id) ? err.request_id : nil)
)
end
err
end
|
._not_found?(err) ⇒ Boolean
142
143
144
145
146
|
# File 'lib/axhub_sdk/data/discover.rb', line 142
def _not_found?(err)
return true if err.is_a?(TableNotFoundError)
err.is_a?(AxHub::Error) && err.respond_to?(:status) && err.status == 404
end
|
._read(obj, key) ⇒ Object
Read an attribute from either a duck-typed object or a Hash.
51
52
53
54
55
56
57
58
|
# File 'lib/axhub_sdk/data/dsl/validation.rb', line 51
def _read(obj, key)
return nil if obj.nil?
return obj.public_send(key) if obj.respond_to?(key)
return obj[key] if obj.is_a?(Hash) && obj.key?(key)
return obj[key.to_s] if obj.is_a?(Hash) && obj.key?(key.to_s)
nil
end
|
._reject_legacy_page_options(after, before, direction, _table_name) ⇒ Object
37
38
39
40
41
42
43
|
# File 'lib/axhub_sdk/data/client.rb', line 37
def _reject_legacy_page_options(after, before, direction, _table_name)
return if after.nil? && before.nil? && direction.nil?
raise LegacyCursorError.new(
'after/before keyset cursors are not supported by the live AX Hub data API; use cursor/page numeric offset pagination'
)
end
|
._resolve_app_id(client, tenant_slug, app_slug) ⇒ Object
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
|
# File 'lib/axhub_sdk/data/discover.rb', line 64
def _resolve_app_id(client, tenant_slug, app_slug)
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000.0
cursor = nil
APP_LOOKUP_MAX_PAGES.times do |page|
if Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000.0 - started_at > APP_LOOKUP_BUDGET_MS
raise IntrospectFailedError.new(
"app lookup budget exceeded (#{APP_LOOKUP_BUDGET_MS}ms) while searching for slug '#{app_slug}' in tenant '#{tenant_slug}'"
)
end
query = { 'tenant_slug' => tenant_slug, 'limit' => APP_LOOKUP_PAGE_SIZE }
query['cursor'] = cursor if cursor
raw = client.request_raw('GET', '/api/v1/apps', query: query, camelize: true)
raw ||= {}
items = raw['items'] || []
match = items.find { |app| app['slug'] == app_slug && app['id'].is_a?(String) }
return match['id'] if match && match['id']
return nil if page.zero? && items.empty?
next_cursor = raw['next_cursor'] || raw['nextCursor']
return nil if next_cursor.nil? || next_cursor == ''
cursor = next_cursor
end
raise ScanLimitExceededError.new(
"App lookup exceeded #{APP_LOOKUP_MAX_PAGES} pages x #{APP_LOOKUP_PAGE_SIZE} apps without finding slug '#{app_slug}'"
)
end
|
._resolve_offset_page(cursor, page, table_name) ⇒ Object
66
67
68
69
70
71
72
73
74
75
|
# File 'lib/axhub_sdk/data/client.rb', line 66
def _resolve_offset_page(cursor, page, table_name)
unless cursor.nil?
_validate_plain_cursor(cursor, table_name)
return cursor.to_i
end
return 1 if page.nil?
raise InvalidCursorError.new('page must be a positive integer') unless page.is_a?(Integer) && page >= 1
page
end
|
._safe_parse(validator, data) ⇒ Object
20
21
22
|
# File 'lib/axhub_sdk/data/dsl/validation.rb', line 20
def _safe_parse(validator, data)
validator.respond_to?(:safe_parse) ? validator.safe_parse(data) : validator.safeParse(data)
end
|
._stringify(value) ⇒ Object
72
73
74
75
76
77
78
79
80
81
82
|
# File 'lib/axhub_sdk/data/where_serializer.rb', line 72
def _stringify(value)
case value
when Time then value.iso8601
when DateTime, Date then value.iso8601
when nil then 'null'
when true then 'true'
when false then 'false'
when String, Integer, Float then value.to_s
else JSON.generate(value)
end
end
|
._stringify_keys(hash) ⇒ Object
62
63
64
|
# File 'lib/axhub_sdk/data/dsl/schema.rb', line 62
def _stringify_keys(hash)
hash.each_with_object({}) { |(k, v), acc| acc[k.to_s] = v }
end
|
._validate_plain_cursor(cursor, _table_name) ⇒ Object
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
# File 'lib/axhub_sdk/data/client.rb', line 45
def _validate_plain_cursor(cursor, _table_name)
if cursor.length > MAX_CURSOR_TOKEN_LENGTH
raise InvalidCursorError.new("Cursor token exceeds maximum size (#{MAX_CURSOR_TOKEN_LENGTH} chars)")
end
if cursor.start_with?('v1:')
raise LegacyCursorError.new(
'Legacy v1: cursor token is not compatible with AX Hub offset-only pagination; restart pagination without cursor'
)
end
if Data.is_v2_cursor(cursor)
raise LegacyCursorError.new(
'v2 keyset cursors are not supported by the live AX Hub data API; restart pagination and use the numeric cursor returned by list()'
)
end
unless cursor.match?(/\A-?\d+\z/)
raise InvalidCursorError.new('Plain cursor must be a positive integer page or a v2: keyset token')
end
parsed = cursor.to_i
raise InvalidCursorError.new('Plain cursor must be a positive integer page or a v2: keyset token') if parsed < 1
end
|
.and_(*clauses) ⇒ Object
71
72
73
|
# File 'lib/axhub_sdk/data/dsl/ops.rb', line 71
def and_(*clauses)
{ op: :and, clauses: clauses }
end
|
.assert_safe_like_pattern(pattern) ⇒ Object
Reject LIKE patterns that translate to catastrophic-backtracking regex shapes (mirrors node assertSafeLikePattern).
35
36
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
|
# File 'lib/axhub_sdk/data/dsl/ops.rb', line 35
def assert_safe_like_pattern(pattern)
if pattern.length > MAX_LIKE_PATTERN_LENGTH
raise ValidationError.new("LIKE pattern exceeds #{MAX_LIKE_PATTERN_LENGTH} chars; refuse to compile", 'like_pattern_too_long')
end
run_of_wildcards = 0
segments = 0
i = 0
n = pattern.length
while i < n
ch = pattern[i]
if ch == '\\'
i += 2
run_of_wildcards = 0
next
end
if ch == '%'
run_of_wildcards += 1
if run_of_wildcards >= MAX_CONSECUTIVE_WILDCARDS
raise ValidationError.new("LIKE pattern has #{run_of_wildcards} consecutive '%'; refuse to compile (ReDoS guard)", 'like_pattern_redos')
end
else
segments += 1 if run_of_wildcards == 1
run_of_wildcards = 0
end
i += 1
end
if segments > MAX_LIKE_ALTERNATION_SEGMENTS
raise ValidationError.new("LIKE pattern has #{segments} '%X%' alternation segments; refuse to compile (ReDoS guard)", 'like_pattern_redos')
end
end
|
.define_schema(table_or_input, columns = nil, validate: nil) ⇒ Object
Define a data table schema. Two call shapes mirror node’s two overloads:
define_schema("orders", { "id" => "uuid", "total" => "number" })
define_schema({ "table" => "orders", "columns" => {...} }, validate: ...)
An existing DataTableSchema is re-wrapped, optionally attaching ‘validate`.
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
# File 'lib/axhub_sdk/data/dsl/schema.rb', line 35
def define_schema(table_or_input, columns = nil, validate: nil)
if table_or_input.is_a?(DataTableSchema)
return DataTableSchema.new(
table: table_or_input.table,
columns: table_or_input.columns,
cols: table_or_input.cols,
validate: validate.nil? ? table_or_input.validate : validate
)
end
if table_or_input.is_a?(Hash)
h = _stringify_keys(table_or_input)
table = h['table']
shape = _stringify_keys(h['columns'])
else
table = table_or_input
raise ArgumentError, 'define_schema requires columns when called with a table name' if columns.nil?
shape = _stringify_keys(columns)
end
cols = shape.each_with_object({}) do |(name, definition), acc|
acc[name] = DataColumn.new(table: table, name: name, definition: definition)
end
DataTableSchema.new(table: table, columns: shape, cols: cols, validate: validate)
end
|
.escape_like(value) ⇒ Object
27
28
29
30
31
|
# File 'lib/axhub_sdk/data/dsl/ops.rb', line 27
def escape_like(value)
return value if value == ''
value.gsub(ESCAPE_LIKE_RE) { |m| "\\#{m}" }
end
|
.fetch_discovered_schema(client, tenant_slug, app_slug, table, fresh: nil, ttl_ms: nil) ⇒ Object
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
# File 'lib/axhub_sdk/data/discover.rb', line 31
def fetch_discovered_schema(client, tenant_slug, app_slug, table, fresh: nil, ttl_ms: nil)
begin
_fetch_app_id_inspect(client, tenant_slug, app_slug, table)
rescue StandardError => err
begin
_fetch_slug_inspect(client, tenant_slug, app_slug, table)
rescue StandardError
raise _normalize_discover_error(err, table)
end
end
end
|
.is_v2_cursor(token) ⇒ Object
66
67
68
|
# File 'lib/axhub_sdk/data/pagination.rb', line 66
def is_v2_cursor(token)
token.is_a?(String) && token.start_with?('v2:')
end
|
.list_all(fetcher, opts = {}) ⇒ Object
Drive a paginated fetcher to exhaustion, yielding each item and a drift marker when the backend total grows mid-iteration (mirrors node listAll). Returns an Enumerator when no block is given (idiomatic Ruby).
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
|
# File 'lib/axhub_sdk/data/pagination.rb', line 73
def list_all(fetcher, opts = {})
return enum_for(:list_all, fetcher, opts) unless block_given?
cursor = opts[:cursor]
initial_total = nil
last_total = nil
loop do
page = fetcher.call(page_size: opts[:page_size], cursor: cursor)
unless page.total.nil?
if initial_total.nil?
initial_total = page.total
last_total = page.total
else
base = last_total.nil? ? initial_total : last_total
if page.total > base
yield ListAllItem.new(type: :drift, added_since: page.total - base)
last_total = page.total
end
end
end
page.items.each { |item| yield ListAllItem.new(type: :item, value: item) }
return if page.next_cursor.nil?
cursor = page.next_cursor
end
end
|
.normalize_order_by(order_by) ⇒ Object
order_by = String | Array<{ field: String, dir?: “asc”|“desc” }>
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
# File 'lib/axhub_sdk/data/pagination.rb', line 31
def normalize_order_by(order_by)
if order_by.is_a?(String)
fields = []
order_by.split(',').each do |part|
trimmed = part.strip
f = if trimmed.start_with?('-')
{ 'field' => trimmed[1..], 'dir' => 'desc' }
elsif trimmed.start_with?('+')
{ 'field' => trimmed[1..], 'dir' => 'asc' }
else
{ 'field' => trimmed, 'dir' => 'asc' }
end
fields << f unless f['field'].nil? || f['field'].empty?
end
elsif order_by && !order_by.empty?
fields = order_by.map do |p|
h = p.transform_keys(&:to_s)
{ 'field' => h['field'], 'dir' => h.fetch('dir', 'asc') }
end
else
fields = []
end
if !fields.empty? && fields.none? { |f| f['field'] == 'id' }
fields << { 'field' => 'id', 'dir' => 'asc' }
end
fields
end
|
.not_(clause) ⇒ Object
79
80
81
|
# File 'lib/axhub_sdk/data/dsl/ops.rb', line 79
def not_(clause)
{ op: :not, clause: clause }
end
|
.or_(*clauses) ⇒ Object
75
76
77
|
# File 'lib/axhub_sdk/data/dsl/ops.rb', line 75
def or_(*clauses)
{ op: :or, clauses: clauses }
end
|
.project_row(row, select) ⇒ Object
38
39
40
41
42
|
# File 'lib/axhub_sdk/data/projection.rb', line 38
def project_row(row, select)
return row.dup if select.nil?
select.each_with_object({}) { |k, acc| acc[k] = row[k] if row.key?(k) }
end
|
.project_rows(rows, select) ⇒ Object
44
45
46
47
48
|
# File 'lib/axhub_sdk/data/projection.rb', line 44
def project_rows(rows, select)
return rows.map(&:dup) if select.nil?
rows.map { |r| project_row(r, select) }
end
|
.raw(sql, params = nil) ⇒ Object
67
68
69
|
# File 'lib/axhub_sdk/data/dsl/ops.rb', line 67
def raw(sql, params = nil)
params.nil? ? { op: :raw, sql: sql } : { op: :raw, sql: sql, params: params }
end
|
.run_schema_validation(schema, data, mode) ⇒ Object
Validate ‘data` against schema.validate before any network request. `mode` is “insert” or “update” (update uses `partial` when available).
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
# File 'lib/axhub_sdk/data/dsl/validation.rb', line 26
def run_schema_validation(schema, data, mode)
validator = schema&.validate
return if validator.nil?
unless validator_like?(validator)
raise AxHub::Error.new(
category: 'configuration', code: 'validator_missing',
message: 'define_schema validate option requires a schema-like object with safe_parse'
)
end
effective = validator
effective = validator.partial if mode == 'update' && validator.respond_to?(:partial)
result = _safe_parse(effective, data)
success = _read(result, :success)
return if success
error = _read(result, :error)
issues = _read(error, :issues) || []
count = issues.empty? ? 1 : issues.length
raise ValidationError.new("#{count} validation failure#{count == 1 ? '' : 's'} before network request", 'validation_failed')
end
|
.schema_cache_key(tenant_slug, app_slug, table) ⇒ Object
22
23
24
|
# File 'lib/axhub_sdk/data/schema_cache.rb', line 22
def schema_cache_key(tenant_slug, app_slug, table)
"#{tenant_slug}/#{app_slug}/#{table}"
end
|
.schema_from_inspect_result(table, raw) ⇒ Object
115
116
117
118
119
120
121
122
123
124
125
126
127
128
|
# File 'lib/axhub_sdk/data/discover.rb', line 115
def schema_from_inspect_result(table, raw)
raw ||= {}
columns = raw['columns'] || []
shape = {}
columns.each do |column|
name = column['name']
next if FORBIDDEN_COLUMN_NAMES.include?(name)
next unless name.is_a?(String) && COLUMN_NAME_RE.match?(name)
shape[name] = _column_type_to_def(column['type'])
end
table_name = raw['tableName'] || raw['table_name'] || raw['name'] || table
Data.define_schema({ 'table' => table_name, 'columns' => shape })
end
|
.serialize_order_by(order_by) ⇒ Object
59
60
61
62
63
64
|
# File 'lib/axhub_sdk/data/pagination.rb', line 59
def serialize_order_by(order_by)
normalized = normalize_order_by(order_by)
return (order_by.is_a?(String) ? order_by : nil) if normalized.empty?
normalized.map { |f| "#{f['dir'] == 'desc' ? '-' : ''}#{f['field']}" }.join(',')
end
|
.serialize_select(select) ⇒ Object
16
17
18
19
20
|
# File 'lib/axhub_sdk/data/projection.rb', line 16
def serialize_select(select)
return nil if select.nil?
select.join(',')
end
|
.serialize_where(expr) ⇒ Object
21
22
23
24
25
26
27
28
29
|
# File 'lib/axhub_sdk/data/where_serializer.rb', line 21
def serialize_where(expr)
return {} if expr.nil?
out = {}
_collect_pushable_filters(expr, allow_and: true).each do |f|
_append_query(out, f[:column], f[:value])
end
out
end
|
.validate_select_columns(schema, select) ⇒ Object
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
# File 'lib/axhub_sdk/data/projection.rb', line 22
def validate_select_columns(schema, select)
return if select.nil?
if select.length.zero?
raise ValidationError.new('select must include at least one column; omit select to fetch full rows', 'select_empty')
end
return if schema.nil?
allowed = schema.columns.keys
invalid = select.reject { |c| allowed.include?(c) }
return if invalid.empty?
plural = invalid.length == 1 ? '' : 's'
raise ValidationError.new("select contains unknown column#{plural}: #{invalid.join(', ')}", 'select_unknown_column')
end
|
.validator_like?(value) ⇒ Boolean
16
17
18
|
# File 'lib/axhub_sdk/data/dsl/validation.rb', line 16
def validator_like?(value)
!value.nil? && (value.respond_to?(:safe_parse) || value.respond_to?(:safeParse))
end
|
.where(column) ⇒ Object
Start a predicate for a column. Accepts a DataColumn, a String, or a Symbol.
where(:status).eq("paid")
where("status").eq("paid")
where(schema.cols["status"]).eq("paid")
Block form (idiomatic Ruby): yields the builder and returns its result, so the bare-atom expr can be written without a trailing chain.
where(:status) { |c| c.eq("paid") }
93
94
95
96
97
98
99
|
# File 'lib/axhub_sdk/data/dsl/ops.rb', line 93
def where(column)
name = column.is_a?(DataColumn) ? column.name : column.to_s
builder = WhereBuilder.new(name)
return yield(builder) if block_given?
builder
end
|