Loading workspace insights... Statistics interval
7 days30 daysLatest CI Pipeline Executions
4e107d7c Changed the members CSV importer to only write a file for queued imports
ref BER-3811
prepare() re-serialised the parsed CSV, wrote it to content/data, and perform()
read it straight back off disk. Nothing ever deleted that file, so every import
left one behind — on Ghost(Pro) that lands on a shared, persistent volume with
no cleanup and no per-site quota. prepare() now returns the parsed rows and
perform() takes them, and a file is written only where one is genuinely needed:
a queued import outlives the request that started it, and the uploaded CSV is
deleted the moment the response finishes, so its rows are spooled to the system
temp directory and the job that reads them deletes them, whether it succeeded
or failed. The spool is JSON rather than re-serialised CSV because CSV drops
any column outside the member model's own set and flattens every value to a
string, so reading it back would need the same coercion the parse already did.
The caller's header mapping is composed with the model's field mapping before
parsing rather than after, because the parser types a value by the name it
lands on, so a blank subscribed cell still reads as subscribed. Where two
columns claim one model field the first now wins, which is a chosen rule rather
than the previous behaviour.
Three latent bugs go with it: a CSV with headers but no data rows returned a
500, a prepared file leaked on every import, and a global label containing a
comma was split into two labels by the serialise and re-parse. Two changes to
stored data are deliberate: values were previously persisted with the CSV
formula-escape apostrophe still attached, so a member named -5 was stored as
'-5, and columns the caller maps but the member model does not know now reach
the import instead of being erased. 09b8a119 Changed the members CSV importer to only write a file for queued imports
ref BER-3811
prepare() re-serialised the parsed CSV, wrote it to content/data, and perform()
read it straight back off disk. Nothing ever deleted that file, so every import
left one behind — on Ghost(Pro) that lands on a shared, persistent volume with
no cleanup and no per-site quota. prepare() now returns the parsed rows and
perform() takes them, and a file is written only where one is genuinely needed:
a queued import outlives the request that started it, and the uploaded CSV is
deleted the moment the response finishes, so its rows are spooled to the system
temp directory and the job that reads them deletes them, whether it succeeded
or failed. The spool is JSON rather than re-serialised CSV because CSV drops
any column outside the member model's own set and flattens every value to a
string, so reading it back would need the same coercion the parse already did.
The caller's header mapping is composed with the model's field mapping before
parsing rather than after, because the parser types a value by the name it
lands on, so a blank subscribed cell still reads as subscribed. Where two
columns claim one model field the first now wins, which is a chosen rule rather
than the previous behaviour.
Three latent bugs go with it: a CSV with headers but no data rows returned a
500, a prepared file leaked on every import, and a global label containing a
comma was split into two labels by the serialise and re-parse. Two changes to
stored data are deliberate: values were previously persisted with the CSV
formula-escape apostrophe still attached, so a member named -5 was stored as
'-5, and columns the caller maps but the member model does not know now reach
the import instead of being erased. 45120b08 Changed the members CSV importer to only write a file for queued imports
ref BER-3811
prepare() re-serialised the parsed CSV, wrote it to content/data, and perform()
read it straight back off disk. Nothing ever deleted that file, so every import
left one behind — on Ghost(Pro) that lands on a shared, persistent volume with
no cleanup and no per-site quota. prepare() now returns the parsed rows and
perform() takes them, and a file is written only where one is genuinely needed:
a queued import outlives the request that started it, and the uploaded CSV is
deleted the moment the response finishes, so its rows are spooled to the system
temp directory and the job that reads them deletes them, whether it succeeded
or failed. The spool is JSON rather than re-serialised CSV because CSV drops
any column outside the member model's own set and flattens every value to a
string, so reading it back would need the same coercion the parse already did.
The caller's header mapping is composed with the model's field mapping before
parsing rather than after, because the parser types a value by the name it
lands on, so a blank subscribed cell still reads as subscribed. Where two
columns claim one model field the first now wins, which is a chosen rule rather
than the previous behaviour.
Three latent bugs go with it: a CSV with headers but no data rows returned a
500, a prepared file leaked on every import, and a global label containing a
comma was split into two labels by the serialise and re-parse. Two changes to
stored data are deliberate: values were previously persisted with the CSV
formula-escape apostrophe still attached, so a member named -5 was stored as
'-5, and columns the caller maps but the member model does not know now reach
the import instead of being erased. a7601b89 Changed the members CSV importer to only write a file for queued imports
ref BER-3811
prepare() re-serialised the parsed CSV, wrote it to content/data, and perform()
read it straight back off disk. Nothing ever deleted that file, so every import
left one behind — on Ghost(Pro) that lands on a shared, persistent volume with
no cleanup and no per-site quota. prepare() now returns the parsed rows and
perform() takes them, and a file is written only where one is genuinely needed:
a queued import outlives the request that started it, and the uploaded CSV is
deleted the moment the response finishes, so its rows are spooled to the system
temp directory as newline-delimited JSON and the job that reads them deletes
them, whether it succeeded or failed. JSON rather than re-serialised CSV
because CSV drops any column outside the member model's own set, and a line at
a time so a large import cannot run into V8's maximum string length on the one
path that exists to handle large imports.
The caller's header mapping is composed with the model's field mapping before
parsing rather than after, because the parser types a value by the name it
lands on, so a blank subscribed cell still reads as subscribed. Where two
columns claim one model field the first now wins, which is a chosen rule rather
than the previous behaviour.
Three latent bugs go with it: a CSV with headers but no data rows returned a
500, a prepared file leaked on every import, and a global label containing a
comma was split into two labels by the serialise and re-parse. Two changes to
stored data are deliberate: values were previously persisted with the CSV
formula-escape apostrophe still attached, so a member named -5 was stored as
'-5, and columns the caller maps but the member model does not know now reach
the import instead of being erased. 5c695a0e Changed the members CSV importer to only write a file for queued imports
ref BER-3811
prepare() re-serialised the parsed CSV, wrote it to content/data, and perform()
read it straight back off disk. Nothing ever deleted that file, so every import
left one behind — on Ghost(Pro) that lands on a shared, persistent volume with
no cleanup and no per-site quota. prepare() now returns the parsed rows and
perform() takes them, and a file is written only where one is genuinely needed:
a queued import outlives the request that started it, and the uploaded CSV is
deleted the moment the response finishes, so its rows are spooled to the system
temp directory as newline-delimited JSON and the job that reads them owns
deleting them. JSON rather than re-serialised CSV because CSV drops any column
outside the member model's own set, and a line at a time so a large import
cannot run into V8's maximum string length on the one path that exists to
handle large imports. A failed import keeps its spool and logs the path, since
the uploaded CSV is gone by then and those rows are the only remaining copy of
what was asked for.
The caller's header mapping is composed with the model's field mapping before
parsing rather than after, because the parser types a value by the name it
lands on, so a blank subscribed cell still reads as subscribed. Where two
columns claim one model field the first now wins, which is a chosen rule rather
than the previous behaviour.
Three latent bugs go with it: a CSV with headers but no data rows returned a
500, a prepared file leaked on every import, and a global label containing a
comma was split into two labels by the serialise and re-parse. Two changes to
stored data are deliberate: values were previously persisted with the CSV
formula-escape apostrophe still attached, so a member named -5 was stored as
'-5, and columns the caller maps but the member model does not know now reach
the import instead of being erased. b194b52b Changed the members CSV importer to only write a file for queued imports
ref BER-3811
prepare() re-serialised the parsed CSV, wrote it to content/data, and perform()
read it straight back off disk. Nothing ever deleted that file, so every import
left one behind — on Ghost(Pro) that lands on a shared, persistent volume with
no cleanup and no per-site quota. prepare() now returns the parsed rows and
perform() takes them, and a file is written only where one is genuinely needed:
a queued import outlives the request that started it, and the uploaded CSV is
deleted the moment the response finishes, so its rows are spooled to the system
temp directory as newline-delimited JSON and the job that reads them owns
deleting them. JSON rather than re-serialised CSV because CSV drops any column
outside the member model's own set, and a line at a time so a large import
cannot run into V8's maximum string length on the one path that exists to
handle large imports. A failed import keeps its spool and logs the path, since
the uploaded CSV is gone by then and those rows are the only remaining copy of
what was asked for.
The caller's header mapping is composed with the model's field mapping before
parsing rather than after, because the parser types a value by the name it
lands on, so a blank subscribed cell still reads as subscribed. Where two
columns claim one model field the first now wins, which is a chosen rule rather
than the previous behaviour.
Three latent bugs go with it: a CSV with headers but no data rows returned a
500, a prepared file leaked on every import, and a global label containing a
comma was split into two labels by the serialise and re-parse. Two changes to
stored data are deliberate: values were previously persisted with the CSV
formula-escape apostrophe still attached, so a member named -5 was stored as
'-5, and columns the caller maps but the member model does not know now reach
the import instead of being erased. d29e6dba Removed the disk round-trip from the members CSV importer
ref BER-3811
prepare() re-serialised the parsed CSV, wrote it to content/data, and perform()
read it straight back off disk. Nothing ever deleted that file, so every import
left one behind — on Ghost(Pro) that lands on a shared, persistent volume with
no cleanup and no per-site quota. prepare() now returns the parsed rows and
perform() takes them, and a file is written only where one is genuinely needed:
a queued import outlives the request that started it, and the uploaded CSV is
deleted the moment the response finishes, so its rows are spooled to the system
temp directory as newline-delimited JSON and the job that reads them owns
deleting them. JSON rather than re-serialised CSV because CSV drops any column
outside the member model's own set, and a line at a time so a large import
cannot run into V8's maximum string length on the one path that exists to
handle large imports. The caller's header mapping is composed with the model's
field mapping before parsing rather than after, because the parser types a
value by the name it lands on, so a blank subscribed cell still reads as
subscribed. Where two columns claim one model field the first now wins, which
is a chosen rule rather than the previous behaviour.
Three latent bugs go with it: a CSV with headers but no data rows returned a
500, a prepared file leaked on every import, and a global label containing a
comma was split into two labels by the serialise and re-parse. Two changes to
stored data are deliberate: values were previously persisted with the CSV
formula-escape apostrophe still attached, so a member named -5 was stored as
'-5, and columns the caller maps but the member model does not know now reach
the import instead of being erased. f4777912 Removed the disk round-trip from the members CSV importer
ref BER-3811
prepare() re-serialised the parsed CSV, wrote it to content/data, and perform()
read it straight back off disk. That saved no memory, since parsing already
holds every row in memory; it never crossed a process boundary, since the
offloaded job is an in-process closure; and it left the file behind forever,
since nothing deleted it. prepare() now returns the parsed rows and perform()
takes them. The caller's header mapping is composed with the model's own field
mapping before parsing rather than after, because the parser picks a value's
type from the name it lands on, so a blank subscribed cell still reads as
subscribed rather than null. Three latent bugs go with it: a CSV with headers
but no data rows returned a 500, a prepared file leaked into content/data on
every import, and a global label containing a comma was split into two labels
by the serialise and re-parse. One deliberate behaviour change: values were
previously stored with the CSV formula-escape apostrophe still attached, so a
name of -5 was persisted as '-5, and escaping now stays where it belongs, on
export. The dead storagePath and getTimezone dependencies are gone. a89dfc10 Removed the disk round-trip from the members CSV importer
ref BER-3811
prepare() re-serialised the parsed CSV, wrote it to content/data, and perform()
read it straight back off disk. That saved no memory, since parsing already
holds every row in memory; it never crossed a process boundary, since the
offloaded job is an in-process closure; and it left the file behind forever,
since nothing deleted it. prepare() now returns the parsed rows and perform()
takes them. The caller's header mapping is composed with the model's own field
mapping before parsing rather than after, because the parser picks a value's
type from the name it lands on, so a blank subscribed cell still reads as
subscribed rather than null. Three latent bugs go with it: a CSV with headers
but no data rows returned a 500, a prepared file leaked into content/data on
every import, and a global label containing a comma was split into two labels
by the serialise and re-parse. One deliberate behaviour change: values were
previously stored with the CSV formula-escape apostrophe still attached, so a
name of -5 was persisted as '-5, and escaping now stays where it belongs, on
export. The dead storagePath and getTimezone dependencies are gone.